diff --git a/packages/generator/package.json b/packages/generator/package.json
index 04448f4..bba23d0 100644
--- a/packages/generator/package.json
+++ b/packages/generator/package.json
@@ -37,10 +37,10 @@
},
"dependencies": {
"@scalar/json-magic": "^0.12.4",
- "@scalar/openapi-parser": "^0.25.6"
+ "@scalar/openapi-parser": "^0.25.6",
+ "@scalar/openapi-types": "0.6.1"
},
"devDependencies": {
- "@scalar/openapi-types": "^0.6.1",
"@tanstack/react-query": "catalog:",
"@tanstack/svelte-query": "catalog:",
"@types/node": "catalog:",
@@ -48,8 +48,5 @@
"@vitest/coverage-v8": "catalog:",
"bumpp": "^11.0.1",
"vite-plus": "catalog:"
- },
- "inlinedDependencies": {
- "@scalar/openapi-types": "0.6.1"
}
}
diff --git a/packages/generator/src/cli.ts b/packages/generator/src/cli.ts
index 846939c..e7cae0c 100644
--- a/packages/generator/src/cli.ts
+++ b/packages/generator/src/cli.ts
@@ -1,6 +1,6 @@
#!/usr/bin/env node
import { mkdir, writeFile } from 'node:fs/promises';
-import { basename, extname, resolve } from 'node:path';
+import { resolve } from 'node:path';
import { parseArgs } from 'node:util';
import { bundle } from '@scalar/json-magic/bundle';
@@ -19,22 +19,16 @@ Usage: openapi-gen [options]
URL or local file path to an OpenAPI 3.1 spec (JSON or YAML)
Options:
- -o, --output
Output directory (default: ".")
- --types Types output filename (default: "types.ts")
- --client Client output filename (default: "client.ts")
- --query TanStack Query helpers output filename (default: "query.ts")
- --tanstack-query TanStack Query framework: react or svelte
+ -o, --output Output directory (default: ".")
+ --tanstack-query TanStack Query framework: react or svelte
--no-throw-on-http-error Do not throw HTTPError on non-ok responses
- -h, --help Show this help message
+ -h, --help Show this help message
`;
const { values, positionals } = parseArgs({
args: process.argv.slice(2),
options: {
output: { type: 'string', short: 'o', default: '.' },
- types: { type: 'string', default: 'types.ts' },
- client: { type: 'string', default: 'client.ts' },
- query: { type: 'string', default: 'query.ts' },
'tanstack-query': { type: 'string' },
'no-throw-on-http-error': { type: 'boolean', default: false },
help: { type: 'boolean', short: 'h', default: false },
@@ -49,9 +43,6 @@ if (values.help || positionals.length === 0) {
const input = positionals[0];
const outputDir = resolve(values.output!);
-const typesFile = resolve(outputDir, values.types!);
-const clientFile = resolve(outputDir, values.client!);
-const queryFile = resolve(outputDir, values.query!);
try {
const spec = await bundle(input, {
@@ -59,28 +50,31 @@ try {
plugins: [fetchUrls(), readFiles(), parseJson(), parseYaml()],
});
- const typesImportPath =
- './' + basename(values.types!, extname(values.types!));
- const { types, client, query } = await generateFromObject(
+ const { types, client, query, index } = await generateFromObject(
spec as Record,
{
tanstackQuery: values['tanstack-query'] as QueryFramework | undefined,
- typesImportPath,
throwOnHttpError: !values['no-throw-on-http-error'],
}
);
await mkdir(outputDir, { recursive: true });
+
const writes: Promise[] = [
- writeFile(typesFile, types, 'utf8'),
- writeFile(clientFile, client, 'utf8'),
+ writeFile(resolve(outputDir, 'client.ts'), client, 'utf8'),
+ writeFile(resolve(outputDir, 'index.ts'), index, 'utf8'),
];
- if (query !== null) writes.push(writeFile(queryFile, query, 'utf8'));
+ if (types)
+ writes.push(writeFile(resolve(outputDir, 'types.ts'), types, 'utf8'));
+ if (query !== null)
+ writes.push(writeFile(resolve(outputDir, 'query.ts'), query, 'utf8'));
await Promise.all(writes);
- console.info(`types → ${typesFile}`);
- console.info(`client → ${clientFile}`);
- if (query !== null) console.info(`query → ${queryFile}`);
+ if (types) console.info(`types → ${resolve(outputDir, 'types.ts')}`);
+ console.info(`client → ${resolve(outputDir, 'client.ts')}`);
+ if (query !== null)
+ console.info(`query → ${resolve(outputDir, 'query.ts')}`);
+ console.info(`index → ${resolve(outputDir, 'index.ts')}`);
} catch (err) {
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
process.exit(1);
diff --git a/packages/generator/src/client.ts b/packages/generator/src/client.ts
index a791fc4..4db3735 100644
--- a/packages/generator/src/client.ts
+++ b/packages/generator/src/client.ts
@@ -250,7 +250,7 @@ export function generateClient(
: '';
return (
- `/* eslint-disable */\n` +
+ `/* eslint-disable */\n/* prettier-ignore-start */\n` +
importLine +
preamble +
[
diff --git a/packages/generator/src/index.ts b/packages/generator/src/index.ts
index caffa25..2f2bb11 100644
--- a/packages/generator/src/index.ts
+++ b/packages/generator/src/index.ts
@@ -16,6 +16,8 @@ export interface GenerateResult {
client: string;
/** TanStack Query helper functions generated from paths, or null if no framework was specified */
query: string | null;
+ /** Barrel index.ts that re-exports from types, client, and query */
+ index: string;
}
export interface GenerateOptions {
@@ -58,14 +60,19 @@ export async function generateFromObject(
const schemas: Record =
spec31.components?.schemas ?? {};
const paths: OpenAPIV3_1.PathsObject = spec31.paths ?? {};
- return {
- types: generateTypes(schemas),
- client: generateClient(paths, {
- typesImportPath: options?.typesImportPath,
- throwOnHttpError: options?.throwOnHttpError,
- }),
- query: options?.tanstackQuery
- ? generateQuery(paths, options.tanstackQuery)
- : null,
- };
+ const types = generateTypes(schemas);
+ const client = generateClient(paths, {
+ typesImportPath: options?.typesImportPath,
+ throwOnHttpError: options?.throwOnHttpError,
+ });
+ const query = options?.tanstackQuery
+ ? generateQuery(paths, options.tanstackQuery)
+ : null;
+
+ const indexLines = ['/* eslint-disable */', '/* prettier-ignore-start */'];
+ if (types) indexLines.push(`export * from './types';`);
+ indexLines.push(`export * from './client';`);
+ if (query !== null) indexLines.push(`export * from './query';`);
+
+ return { types, client, query, index: indexLines.join('\n') + '\n' };
}
diff --git a/packages/generator/src/node.ts b/packages/generator/src/node.ts
index 5cb0df4..a7d5be9 100644
--- a/packages/generator/src/node.ts
+++ b/packages/generator/src/node.ts
@@ -1,3 +1,6 @@
+import { mkdir, writeFile } from 'node:fs/promises';
+import { resolve } from 'node:path';
+
import { bundle } from '@scalar/json-magic/bundle';
import {
fetchUrls,
@@ -12,6 +15,11 @@ import {
type GenerateResult,
} from './index.js';
+export interface WriteOptions {
+ /** Output directory (default: `'.'`) */
+ outDir?: string;
+}
+
/**
* Generate TypeScript types and API client from a local file path, URL,
* or already-parsed object.
@@ -35,3 +43,43 @@ export async function generateFrom(
});
return generateFromObject(spec as Record, options);
}
+
+/**
+ * Generate and write output files to disk.
+ *
+ * Writes `client.ts` and `index.ts` always; `types.ts` when schemas exist;
+ * `query.ts` when a TanStack Query framework is specified.
+ *
+ * @returns Resolved paths of the written files.
+ */
+export async function generateAndWrite(
+ input: string | Record,
+ options?: GenerateOptions & WriteOptions
+): Promise<{ types?: string; client: string; query?: string; index: string }> {
+ const { outDir, ...generateOptions } = options ?? {};
+ const outputDir = resolve(outDir ?? '.');
+
+ const { types, client, query, index } = await generateFrom(
+ input,
+ generateOptions
+ );
+
+ await mkdir(outputDir, { recursive: true });
+
+ const writes: Promise[] = [
+ writeFile(resolve(outputDir, 'client.ts'), client, 'utf8'),
+ writeFile(resolve(outputDir, 'index.ts'), index, 'utf8'),
+ ];
+ if (types)
+ writes.push(writeFile(resolve(outputDir, 'types.ts'), types, 'utf8'));
+ if (query !== null)
+ writes.push(writeFile(resolve(outputDir, 'query.ts'), query, 'utf8'));
+ await Promise.all(writes);
+
+ return {
+ ...(types ? { types: resolve(outputDir, 'types.ts') } : {}),
+ client: resolve(outputDir, 'client.ts'),
+ ...(query !== null ? { query: resolve(outputDir, 'query.ts') } : {}),
+ index: resolve(outputDir, 'index.ts'),
+ };
+}
diff --git a/packages/generator/src/query.ts b/packages/generator/src/query.ts
index d51d467..13cef96 100644
--- a/packages/generator/src/query.ts
+++ b/packages/generator/src/query.ts
@@ -177,6 +177,7 @@ export function generateQuery(
const header = [
`/* eslint-disable */`,
+ `/* prettier-ignore-start */`,
`import type { apiClient } from './client';`,
`import type { ${queryOptionsType}, ${mutationOptionsType} } from '${pkg}';`,
``,
diff --git a/packages/generator/src/schema.ts b/packages/generator/src/schema.ts
index 29610fb..b29efea 100644
--- a/packages/generator/src/schema.ts
+++ b/packages/generator/src/schema.ts
@@ -174,7 +174,7 @@ function generateSchemaType(
return `${jsdocLines.join('\n')}\nexport type ${name} = ${schemaToTypeString(s)};`;
}
-const ESLINT_DISABLE = '/* eslint-disable */';
+const FILE_HEADER = '/* eslint-disable */\n/* prettier-ignore-start */';
export function generateTypes(
schemas: Record
@@ -182,5 +182,5 @@ export function generateTypes(
const body = Object.entries(schemas)
.map(([name, schema]) => generateSchemaType(name, schema))
.join('\n\n');
- return body ? `${ESLINT_DISABLE}\n${body}` : '';
+ return body ? `${FILE_HEADER}\n${body}` : '';
}
diff --git a/packages/generator/tests/client.test.ts b/packages/generator/tests/client.test.ts
index eaa56ec..7325559 100644
--- a/packages/generator/tests/client.test.ts
+++ b/packages/generator/tests/client.test.ts
@@ -124,6 +124,7 @@ describe('generateClient', () => {
expect(generateClient({ '/users': { get: { responses: {} } } }))
.toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
export class HTTPError extends Error {
readonly status: number;
readonly statusText: string;
@@ -170,6 +171,7 @@ describe('generateClient', () => {
generateClient({ '/auth/verify-email': { post: { responses: {} } } })
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
export class HTTPError extends Error {
readonly status: number;
readonly statusText: string;
@@ -217,6 +219,7 @@ describe('generateClient', () => {
expect(generateClient({ '/api/_public': { get: { responses: {} } } }))
.toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
export class HTTPError extends Error {
readonly status: number;
readonly statusText: string;
@@ -276,6 +279,7 @@ describe('generateClient', () => {
})
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
export class HTTPError extends Error {
readonly status: number;
readonly statusText: string;
@@ -333,6 +337,7 @@ describe('generateClient', () => {
})
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
export class HTTPError extends Error {
readonly status: number;
readonly statusText: string;
@@ -380,6 +385,7 @@ describe('generateClient', () => {
expect(generateClient({ '/users/{id}': { get: { responses: {} } } }))
.toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
export class HTTPError extends Error {
readonly status: number;
readonly statusText: string;
@@ -447,6 +453,7 @@ describe('generateClient', () => {
})
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
import type { User } from './types';
export class HTTPError extends Error {
@@ -496,6 +503,7 @@ describe('generateClient', () => {
expect(generateClient({ '/ping': { get: { responses: {} } } }))
.toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
export class HTTPError extends Error {
readonly status: number;
readonly statusText: string;
@@ -559,6 +567,7 @@ describe('generateClient', () => {
})
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
export class HTTPError extends Error {
readonly status: number;
readonly statusText: string;
@@ -622,6 +631,7 @@ describe('generateClient', () => {
})
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
export class HTTPError extends Error {
readonly status: number;
readonly statusText: string;
@@ -686,6 +696,7 @@ describe('generateClient', () => {
})
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
export class HTTPError extends Error {
readonly status: number;
readonly statusText: string;
@@ -748,6 +759,7 @@ describe('generateClient', () => {
})
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
export class HTTPError extends Error {
readonly status: number;
readonly statusText: string;
@@ -820,6 +832,7 @@ describe('generateClient', () => {
})
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
export class HTTPError extends Error {
readonly status: number;
readonly statusText: string;
diff --git a/packages/generator/tests/generate.test.ts b/packages/generator/tests/generate.test.ts
index f925267..ad93d9b 100644
--- a/packages/generator/tests/generate.test.ts
+++ b/packages/generator/tests/generate.test.ts
@@ -117,6 +117,7 @@ describe('generateFromObject', () => {
expect(result.types).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
/**
* User
*/
@@ -138,6 +139,7 @@ describe('generateFromObject', () => {
expect(result.client).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
import type { User } from './types';
export class HTTPError extends Error {
diff --git a/packages/generator/tests/query.test.ts b/packages/generator/tests/query.test.ts
index 0092253..588a97d 100644
--- a/packages/generator/tests/query.test.ts
+++ b/packages/generator/tests/query.test.ts
@@ -6,6 +6,7 @@ describe('generateQuery', () => {
test('empty paths produces minimal queryClient', () => {
expect(generateQuery({}, 'react')).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
import type { apiClient } from './client';
import type { UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
@@ -24,6 +25,7 @@ describe('generateQuery', () => {
expect(generateQuery({ '/users': { get: { responses: {} } } }, 'react'))
.toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
import type { apiClient } from './client';
import type { UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
@@ -70,6 +72,7 @@ describe('generateQuery', () => {
)
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
import type { apiClient } from './client';
import type { UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
@@ -109,6 +112,7 @@ describe('generateQuery', () => {
)
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
import type { apiClient } from './client';
import type { UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
@@ -167,6 +171,7 @@ describe('generateQuery', () => {
)
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
import type { apiClient } from './client';
import type { UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
@@ -226,6 +231,7 @@ describe('generateQuery', () => {
)
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
import type { apiClient } from './client';
import type { UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
@@ -279,6 +285,7 @@ describe('generateQuery', () => {
)
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
import type { apiClient } from './client';
import type { UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
@@ -315,6 +322,7 @@ describe('generateQuery', () => {
expect(generateQuery({ '/users': { post: { responses: {} } } }, 'react'))
.toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
import type { apiClient } from './client';
import type { UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
@@ -369,6 +377,7 @@ describe('generateQuery', () => {
)
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
import type { apiClient } from './client';
import type { UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
@@ -416,6 +425,7 @@ describe('generateQuery', () => {
)
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
import type { apiClient } from './client';
import type { UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
@@ -453,6 +463,7 @@ describe('generateQuery', () => {
expect(generateQuery({ '/users': { get: { responses: {} } } }, 'svelte'))
.toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
import type { apiClient } from './client';
import type { CreateQueryOptions, CreateMutationOptions } from '@tanstack/svelte-query';
@@ -502,6 +513,7 @@ describe('generateQuery', () => {
)
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
import type { apiClient } from './client';
import type { UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
diff --git a/packages/generator/tests/schema.test.ts b/packages/generator/tests/schema.test.ts
index 108f2c4..d5a499c 100644
--- a/packages/generator/tests/schema.test.ts
+++ b/packages/generator/tests/schema.test.ts
@@ -227,6 +227,7 @@ describe('generateTypes', () => {
})
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
/**
* User
*/
@@ -247,6 +248,7 @@ describe('generateTypes', () => {
})
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
/**
* A platform user
*/
@@ -258,6 +260,7 @@ describe('generateTypes', () => {
expect(generateTypes({ Status: { type: 'string', example: 'active' } }))
.toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
/**
* Status
* @example "active"
@@ -274,6 +277,7 @@ describe('generateTypes', () => {
})
).toMatchInlineSnapshot(`
"/* eslint-disable */
+ /* prettier-ignore-start */
/**
* User
*/
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 86fbebc..4420a62 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -73,10 +73,10 @@ importers:
'@scalar/openapi-parser':
specifier: ^0.25.6
version: 0.25.6
- devDependencies:
'@scalar/openapi-types':
- specifier: ^0.6.1
+ specifier: 0.6.1
version: 0.6.1
+ devDependencies:
'@tanstack/react-query':
specifier: 'catalog:'
version: 5.95.0(react@19.2.4)