From 33e268b0986105f84ee885cd034f4e106c5f93db Mon Sep 17 00:00:00 2001 From: Taiyu Yoshizawa Date: Mon, 23 Mar 2026 20:53:49 +0900 Subject: [PATCH] Add index.ts barrel export and update file headers --- packages/generator/package.json | 7 +--- packages/generator/src/cli.ts | 40 ++++++++----------- packages/generator/src/client.ts | 2 +- packages/generator/src/index.ts | 27 ++++++++----- packages/generator/src/node.ts | 48 +++++++++++++++++++++++ packages/generator/src/query.ts | 1 + packages/generator/src/schema.ts | 4 +- packages/generator/tests/client.test.ts | 13 ++++++ packages/generator/tests/generate.test.ts | 2 + packages/generator/tests/query.test.ts | 12 ++++++ packages/generator/tests/schema.test.ts | 4 ++ pnpm-lock.yaml | 4 +- 12 files changed, 121 insertions(+), 43 deletions(-) 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)