fork of hey-api/openapi-ts because I need some additional things
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor: cli interface

Lubos 4f52bce7 9a2e0c87

+656 -527
+5
.changeset/bright-falcons-chew.md
··· 1 + --- 2 + '@hey-api/types': patch 3 + --- 4 + 5 + feat: add `ToArray`, `ToReadonlyArray`, and `AnyObject` types
+5
.changeset/clever-toys-dress.md
··· 1 + --- 2 + '@hey-api/openapi-ts': patch 3 + --- 4 + 5 + **cli**: clean up interface
+5
.changeset/fluffy-pens-admire.md
··· 1 + --- 2 + '@hey-api/codegen-core': patch 3 + --- 4 + 5 + **config**: export `loadConfigFile` function (moved from `@hey-api/openapi-ts`)
+5
.changeset/stupid-news-wash.md
··· 1 + --- 2 + '@hey-api/openapi-ts': patch 3 + --- 4 + 5 + **config**: move `loadConfigFile` function to `@hey-api/codegen-core`
+2
.vscode/launch.json
··· 16 16 "cwd": "${workspaceFolder}/dev", 17 17 "runtimeExecutable": "node", 18 18 "program": "${workspaceFolder}/packages/openapi-ts/dist/run.mjs", 19 + "args": [], 19 20 "env": { 20 21 "DEBUG": "false" 21 22 } ··· 28 29 "cwd": "${workspaceFolder}/dev", 29 30 "runtimeExecutable": "node", 30 31 "program": "${workspaceFolder}/packages/openapi-python/dist/run.mjs", 32 + "args": [], 31 33 "env": { 32 34 "DEBUG": "false" 33 35 }
+1
packages/codegen-core/package.json
··· 63 63 "dependencies": { 64 64 "@hey-api/types": "workspace:*", 65 65 "ansi-colors": "4.1.3", 66 + "c12": "3.3.3", 66 67 "color-support": "1.1.3" 67 68 }, 68 69 "peerDependencies": {
+3
packages/codegen-core/src/__tests__/exports.test.ts
··· 8 8 'File', 9 9 'fromRef', 10 10 'fromRefs', 11 + 'detectInteractiveSession', 11 12 'isNode', 12 13 'isNodeRef', 13 14 'isRef', 14 15 'isSymbol', 15 16 'isSymbolRef', 17 + 'loadConfigFile', 16 18 'log', 17 19 'Logger', 20 + 'mergeConfigs', 18 21 'nodeBrand', 19 22 'Project', 20 23 'ref',
+14
packages/codegen-core/src/config/interactive.ts
··· 1 + /** 2 + * Detect if the current session is interactive based on TTY status and environment variables. 3 + * This is used as a fallback when the user doesn't explicitly set the interactive option. 4 + * @internal 5 + */ 6 + export function detectInteractiveSession(): boolean { 7 + return Boolean( 8 + process.stdin.isTTY && 9 + process.stdout.isTTY && 10 + !process.env.CI && 11 + !process.env.NO_INTERACTIVE && 12 + !process.env.NO_INTERACTION, 13 + ); 14 + }
+42
packages/codegen-core/src/config/load.ts
··· 1 + import type { Logger } from '@hey-api/codegen-core'; 2 + import type { AnyObject, MaybeArray } from '@hey-api/types'; 3 + 4 + import { mergeConfigs } from './merge'; 5 + 6 + export async function loadConfigFile<T extends AnyObject>({ 7 + configFile, 8 + logger, 9 + name, 10 + userConfig, 11 + }: { 12 + configFile: string | undefined; 13 + logger: Logger; 14 + name: string; 15 + userConfig: T; 16 + }): Promise<{ 17 + configFile: string | undefined; 18 + configs: ReadonlyArray<T>; 19 + foundConfig: boolean; 20 + }> { 21 + const eventC12 = logger.timeEvent('c12'); 22 + // c12 is ESM-only since v3 23 + const { loadConfig } = await import('c12'); 24 + 25 + const { config: fileConfig, configFile: loadedConfigFile } = await loadConfig< 26 + MaybeArray<T> 27 + >({ 28 + configFile, 29 + name, 30 + }); 31 + eventC12.timeEnd(); 32 + 33 + const fileConfigs = fileConfig instanceof Array ? fileConfig : [fileConfig]; 34 + const mergedConfigs = fileConfigs.map((config) => 35 + mergeConfigs<T>(config, userConfig), 36 + ); 37 + const foundConfig = fileConfigs.some( 38 + (config) => Object.keys(config).length > 0, 39 + ); 40 + 41 + return { configFile: loadedConfigFile, configs: mergedConfigs, foundConfig }; 42 + }
+28
packages/codegen-core/src/config/merge.ts
··· 1 + import type { AnyObject } from '@hey-api/types'; 2 + 3 + function isPlainObject(value: unknown): value is AnyObject { 4 + return typeof value === 'object' && value !== null && !Array.isArray(value); 5 + } 6 + 7 + export function mergeConfigs<T extends AnyObject>( 8 + configA: T | undefined, 9 + configB: T | undefined, 10 + ): T { 11 + const a = (configA || {}) as AnyObject; 12 + const b = (configB || {}) as AnyObject; 13 + 14 + const result: AnyObject = { ...a }; 15 + 16 + for (const key of Object.keys(b)) { 17 + const valueA = a[key]; 18 + const valueB = b[key]; 19 + 20 + if (isPlainObject(valueA) && isPlainObject(valueB)) { 21 + result[key] = mergeConfigs(valueA, valueB); 22 + } else { 23 + result[key] = valueB; 24 + } 25 + } 26 + 27 + return result as T; 28 + }
+3
packages/codegen-core/src/index.ts
··· 5 5 ImportModule, 6 6 } from './bindings'; 7 7 export { nodeBrand, symbolBrand } from './brands'; 8 + export { detectInteractiveSession } from './config/interactive'; 9 + export { loadConfigFile } from './config/load'; 10 + export { mergeConfigs } from './config/merge'; 8 11 export type { 9 12 IProjectRenderMeta as ProjectRenderMeta, 10 13 ISymbolMeta as SymbolMeta,
-1
packages/openapi-python/package.json
··· 77 77 "@hey-api/json-schema-ref-parser": "1.2.2", 78 78 "@hey-api/types": "workspace:*", 79 79 "ansi-colors": "4.1.3", 80 - "c12": "3.3.3", 81 80 "color-support": "1.1.3", 82 81 "commander": "14.0.2", 83 82 "open": "11.0.0",
-120
packages/openapi-python/src/cli.ts
··· 1 - import type { OptionValues } from 'commander'; 2 - import { Command } from 'commander'; 3 - 4 - import { createClient } from '~/index'; 5 - 6 - import pkg from '../package.json' assert { type: 'json' }; 7 - 8 - const stringToBoolean = ( 9 - value: string | undefined, 10 - ): boolean | string | undefined => { 11 - if (value === 'true') return true; 12 - if (value === 'false') return false; 13 - return value; 14 - }; 15 - 16 - const processParams = ( 17 - obj: OptionValues, 18 - booleanKeys: ReadonlyArray<string>, 19 - ): OptionValues => { 20 - for (const key of booleanKeys) { 21 - const value = obj[key]; 22 - if (typeof value === 'string') { 23 - const parsedValue = stringToBoolean(value); 24 - delete obj[key]; 25 - obj[key] = parsedValue; 26 - } 27 - } 28 - return obj; 29 - }; 30 - 31 - export const runCli = async (): Promise<void> => { 32 - const params = new Command() 33 - .name(Object.keys(pkg.bin)[0]!) 34 - .usage('[options]') 35 - .version(pkg.version) 36 - .option('-c, --client <value>', 'HTTP client to generate') 37 - .option('-d, --debug', 'Set log level to debug') 38 - .option('--dry-run [value]', 'Skip writing files to disk?') 39 - .option('-f, --file [value]', 'Path to the config file') 40 - .option( 41 - '-i, --input <value>', 42 - 'OpenAPI specification (path, url, or string content)', 43 - ) 44 - .option('-l, --logs [value]', 'Logs folder') 45 - .option('-o, --output <value>', 'Output folder') 46 - .option('-p, --plugins [value...]', "List of plugins you'd like to use") 47 - .option('-s, --silent', 'Set log level to silent') 48 - .option( 49 - '--no-log-file', 50 - 'Disable writing a log file. Works like --silent but without suppressing console output', 51 - ) 52 - .option( 53 - '-w, --watch [value]', 54 - 'Regenerate the client when the input file changes?', 55 - ) 56 - .parse(process.argv) 57 - .opts(); 58 - 59 - let userConfig: Record<string, unknown>; 60 - 61 - try { 62 - userConfig = processParams(params, ['dryRun', 'logFile']); 63 - 64 - if (userConfig.file) { 65 - userConfig.configFile = userConfig.file; 66 - delete userConfig.file; 67 - } 68 - 69 - if (params.plugins === true) { 70 - userConfig.plugins = []; 71 - } else if (params.plugins) { 72 - userConfig.plugins = params.plugins; 73 - } else if (userConfig.client) { 74 - userConfig.plugins = ['@hey-api/sdk']; 75 - } 76 - 77 - if (userConfig.client) { 78 - (userConfig.plugins as Array<string>).push(userConfig.client as string); 79 - delete userConfig.client; 80 - } 81 - 82 - userConfig.logs = userConfig.logs 83 - ? { 84 - path: userConfig.logs, 85 - } 86 - : {}; 87 - 88 - if (userConfig.debug) { 89 - (userConfig.logs as Record<string, unknown>).level = 'debug'; 90 - delete userConfig.debug; 91 - } else if (userConfig.silent) { 92 - (userConfig.logs as Record<string, unknown>).level = 'silent'; 93 - delete userConfig.silent; 94 - } 95 - 96 - (userConfig.logs as Record<string, unknown>).file = userConfig.logFile; 97 - delete userConfig.logFile; 98 - 99 - if (typeof params.watch === 'string') { 100 - userConfig.watch = Number.parseInt(params.watch, 10); 101 - } 102 - 103 - if (!Object.keys(userConfig.logs as Record<string, unknown>).length) { 104 - delete userConfig.logs; 105 - } 106 - 107 - const context = await createClient( 108 - userConfig as unknown as Required<Parameters<typeof createClient>>[0], 109 - ); 110 - if ( 111 - !context[0]?.config.input.some( 112 - (input) => input.watch && input.watch.enabled, 113 - ) 114 - ) { 115 - process.exit(0); 116 - } 117 - } catch { 118 - process.exit(1); 119 - } 120 - };
+40
packages/openapi-python/src/cli/adapter.ts
··· 1 + // import type { ToArray } from '@hey-api/types'; 2 + 3 + import type { UserConfig } from '~/config/types'; 4 + 5 + import type { CliOptions } from './schema'; 6 + 7 + export const cliToConfig = (cli: CliOptions): Partial<UserConfig> => { 8 + const config: Partial<UserConfig> = {}; 9 + 10 + if (cli.input) config.input = cli.input; 11 + if (cli.output) config.output = cli.output; 12 + if (cli.file) config.configFile = cli.file; 13 + if (cli.dryRun !== undefined) config.dryRun = cli.dryRun; 14 + 15 + // const plugins: ToArray<UserConfig['plugins']> = []; 16 + // if (cli.plugins instanceof Array && cli.plugins.length > 0) { 17 + // plugins.push(...cli.plugins); 18 + // } 19 + // if (cli.client) plugins.push(cli.client); 20 + // if (plugins.length > 0) config.plugins = plugins; 21 + 22 + if (cli.debug || cli.silent || cli.logs || cli.logFile !== undefined) { 23 + config.logs = { 24 + ...(cli.logs && { path: cli.logs }), 25 + ...(cli.debug && { level: 'debug' as const }), 26 + ...(cli.silent && { level: 'silent' as const }), 27 + ...(cli.logFile !== undefined && { file: cli.logFile }), 28 + }; 29 + } 30 + 31 + if (cli.watch !== undefined) { 32 + if (typeof cli.watch === 'string') { 33 + config.watch = Number.parseInt(cli.watch, 10); 34 + } else { 35 + config.watch = cli.watch; 36 + } 37 + } 38 + 39 + return config; 40 + };
+67
packages/openapi-python/src/cli/index.ts
··· 1 + import { Command, CommanderError } from 'commander'; 2 + 3 + import { createClient } from '~/index'; 4 + 5 + import pkg from '../../package.json' assert { type: 'json' }; 6 + import { cliToConfig } from './adapter'; 7 + 8 + const binName = Object.keys(pkg.bin)[0]!; 9 + 10 + const program = new Command() 11 + .name(binName) 12 + .description('Generate Python code from OpenAPI specifications') 13 + .version(pkg.version); 14 + 15 + program 16 + .option( 17 + '-i, --input <path...>', 18 + 'OpenAPI specification (path, URL, or string)', 19 + ) 20 + .option('-o, --output <path...>', 'Output folder(s)') 21 + .option('-c, --client <name>', 'HTTP client to generate') 22 + .option('-p, --plugins [names...]', 'Plugins to use') 23 + .option('-f, --file <path>', 'Path to config file') 24 + .option('-d, --debug', 'Enable debug logging') 25 + .option('-s, --silent', 'Suppress all output') 26 + .option('-l, --logs <path>', 'Logs folder path') 27 + .option('--no-log-file', 'Disable log file output') 28 + .option('--dry-run', 'Skip writing files') 29 + .option('-w, --watch [interval]', 'Watch for changes') 30 + .action(async (options) => { 31 + const config = cliToConfig(options); 32 + 33 + const context = await createClient( 34 + config as Parameters<typeof createClient>[0], 35 + ); 36 + 37 + const hasActiveWatch = context[0]?.config.input.some( 38 + (input) => input.watch?.enabled, 39 + ); 40 + 41 + if (!hasActiveWatch) { 42 + process.exit(0); 43 + } 44 + }); 45 + 46 + export async function runCli(): Promise<void> { 47 + try { 48 + await program.parseAsync(process.argv); 49 + } catch (error) { 50 + if (error instanceof CommanderError && 'code' in error) { 51 + if (error.code === 'commander.optionMissingArgument') { 52 + console.error( 53 + `\nMissing required argument. Run '${binName} --help' for usage.\n`, 54 + ); 55 + } else if (error.code === 'commander.unknownOption') { 56 + console.error( 57 + `\nUnknown option. Run '${binName} --help' for available options.\n`, 58 + ); 59 + } 60 + 61 + process.exit(error.exitCode); 62 + } 63 + 64 + console.error('Unexpected error:', error); 65 + process.exit(1); 66 + } 67 + }
+17
packages/openapi-python/src/cli/schema.ts
··· 1 + import type { MaybeArray } from '@hey-api/types'; 2 + 3 + // import type { PluginClientNames, PluginNames } from "~/plugins/types"; 4 + 5 + export interface CliOptions { 6 + // client?: PluginClientNames; 7 + debug?: boolean; 8 + dryRun?: boolean; 9 + file?: string; 10 + input?: MaybeArray<string>; 11 + logFile?: boolean; 12 + logs?: string; 13 + output?: MaybeArray<string>; 14 + // plugins?: ReadonlyArray<PluginNames>; 15 + silent?: boolean; 16 + watch?: boolean | string; 17 + }
+12 -1
packages/openapi-python/src/config/types.d.ts
··· 1 - // import { MaybeArray } from "@hey-api/types"; 1 + import type { MaybeArray } from '@hey-api/types'; 2 2 3 3 export interface UserConfig { 4 4 /** ··· 28 28 * generate multiple outputs, one for each input. 29 29 */ 30 30 // input: MaybeArray<UserInput | Required<UserInput>['path']>; 31 + input: MaybeArray<string>; 31 32 /** 32 33 * Show an interactive error reporting tool when the program crashes? You 33 34 * generally want to keep this disabled (default). ··· 41 42 * @default process.cwd() 42 43 */ 43 44 // logs?: string | Logs; 45 + logs?: 46 + | string 47 + | { 48 + level?: 'debug' | 'info' | 'warn' | 'error' | 'silent'; 49 + }; 44 50 /** 45 51 * Path to the output folder. 46 52 * ··· 48 54 * generate multiple outputs, one for each input. 49 55 */ 50 56 // output: MaybeArray<string | UserOutput>; 57 + output: MaybeArray<string>; 51 58 /** 52 59 * Customize how the input is parsed and transformed before it's passed to 53 60 * plugins. ··· 68 75 // }; 69 76 // }[PluginNames] 70 77 // >; 78 + /** 79 + * @deprecated use `input.watch` instead 80 + */ 81 + watch?: boolean | number; 71 82 }
+5 -3
packages/openapi-python/src/index.ts
··· 79 79 export { createClient } from '~/generate'; 80 80 81 81 /** 82 - * Type helper for openapi-ts.config.ts, returns {@link MaybeArray<UserConfig>} object(s) 82 + * Type helper for configuration object, returns {@link MaybeArray<UserConfig>} object(s) 83 83 */ 84 - export const defineConfig = async <T extends MaybeArray<UserConfig>>( 84 + export async function defineConfig<T extends MaybeArray<UserConfig>>( 85 85 config: LazyOrAsync<T>, 86 - ): Promise<T> => (typeof config === 'function' ? await config() : config); 86 + ): Promise<T> { 87 + return typeof config === 'function' ? await config() : config; 88 + } 87 89 88 90 export { Logger } from '@hey-api/codegen-core'; 89 91 // export { defaultPaginationKeywords } from '~/config/parser';
-1
packages/openapi-ts-tests/main/test/cli.test.ts
··· 15 15 '--output', 16 16 path.resolve(__dirname, '.gen'), 17 17 '--dry-run', 18 - 'true', 19 18 ]); 20 19 expect(result.error).toBeFalsy(); 21 20 expect(result.status).toBe(0);
-1
packages/openapi-ts/package.json
··· 93 93 "@hey-api/json-schema-ref-parser": "1.2.2", 94 94 "@hey-api/types": "workspace:*", 95 95 "ansi-colors": "4.1.3", 96 - "c12": "3.3.3", 97 96 "color-support": "1.1.3", 98 97 "commander": "14.0.2", 99 98 "open": "11.0.0",
+6 -7
packages/openapi-ts/src/__tests__/cli.test.ts
··· 53 53 process.argv = originalArgv; 54 54 } 55 55 expect(spy).toHaveBeenCalledWith({ 56 - input: 'foo.json', 56 + input: ['foo.json'], 57 57 logs: { 58 58 file: true, 59 59 }, 60 - output: 'bar', 60 + output: ['bar'], 61 61 }); 62 62 }); 63 63 ··· 77 77 logs: { 78 78 file: true, 79 79 }, 80 - plugins: [], 81 80 }); 82 81 }); 83 82 ··· 102 101 }); 103 102 }); 104 103 105 - it('with default plugins', async () => { 104 + it('with client plugin', async () => { 106 105 const originalArgv = process.argv.slice(); 107 106 try { 108 107 process.argv = [ ··· 119 118 logs: { 120 119 file: true, 121 120 }, 122 - plugins: ['@hey-api/typescript', '@hey-api/sdk', 'foo'], 121 + plugins: ['foo'], 123 122 }); 124 123 }); 125 124 ··· 214 213 expect(spy).toHaveBeenCalledWith({ 215 214 configFile: 'bar', 216 215 dryRun: true, 217 - input: 'baz', 216 + input: ['baz'], 218 217 logs: { 219 218 file: true, 220 219 path: 'qux', 221 220 }, 222 - output: 'quux', 221 + output: ['quux'], 223 222 plugins: ['foo'], 224 223 watch: true, 225 224 });
+16 -10
packages/openapi-ts/src/__tests__/interactive.test.ts
··· 1 - import { Logger } from '@hey-api/codegen-core'; 1 + import { 2 + detectInteractiveSession, 3 + Logger, 4 + mergeConfigs, 5 + } from '@hey-api/codegen-core'; 2 6 import { afterEach, describe, expect, it } from 'vitest'; 3 7 4 - import { detectInteractiveSession, initConfigs } from '~/config/init'; 5 - import { mergeConfigs } from '~/config/merge'; 8 + import { resolveJobs } from '~/config/init'; 6 9 7 10 describe('interactive config', () => { 8 11 it('should use detectInteractiveSession when not provided', async () => { 9 - const result = await initConfigs({ 12 + const result = await resolveJobs({ 10 13 logger: new Logger(), 11 14 userConfigs: [ 12 15 { ··· 17 20 }); 18 21 19 22 // In test environment, TTY is typically not available, so it should be false 20 - expect(result.results[0]?.config.interactive).toBe(false); 23 + expect(result.jobs[0]?.config.interactive).toBe(false); 21 24 }); 22 25 23 26 it('should respect user config when set to true', async () => { 24 - const result = await initConfigs({ 27 + const result = await resolveJobs({ 25 28 logger: new Logger(), 26 29 userConfigs: [ 27 30 { ··· 32 35 ], 33 36 }); 34 37 35 - expect(result.results[0]?.config.interactive).toBe(true); 38 + expect(result.jobs[0]?.config.interactive).toBe(true); 36 39 }); 37 40 38 41 it('should respect user config when set to false', async () => { 39 - const result = await initConfigs({ 42 + const result = await resolveJobs({ 40 43 logger: new Logger(), 41 44 userConfigs: [ 42 45 { ··· 47 50 ], 48 51 }); 49 52 50 - expect(result.results[0]?.config.interactive).toBe(false); 53 + expect(result.jobs[0]?.config.interactive).toBe(false); 51 54 }); 52 55 53 56 it('should allow file config to set interactive when CLI does not provide it', () => { ··· 76 79 }; 77 80 78 81 // After fix: file config's interactive should be preserved 79 - const mergedCorrect = mergeConfigs(fileConfig, cliConfigWithoutInteractive); 82 + const mergedCorrect = mergeConfigs<Partial<typeof fileConfig>>( 83 + fileConfig, 84 + cliConfigWithoutInteractive, 85 + ); 80 86 expect(mergedCorrect.interactive).toBe(false); 81 87 82 88 // Before fix: CLI's auto-detected interactive would override file config
-120
packages/openapi-ts/src/cli.ts
··· 1 - import type { OptionValues } from 'commander'; 2 - import { Command } from 'commander'; 3 - 4 - import { createClient } from '~/index'; 5 - 6 - import pkg from '../package.json' assert { type: 'json' }; 7 - 8 - const stringToBoolean = ( 9 - value: string | undefined, 10 - ): boolean | string | undefined => { 11 - if (value === 'true') return true; 12 - if (value === 'false') return false; 13 - return value; 14 - }; 15 - 16 - const processParams = ( 17 - obj: OptionValues, 18 - booleanKeys: ReadonlyArray<string>, 19 - ): OptionValues => { 20 - for (const key of booleanKeys) { 21 - const value = obj[key]; 22 - if (typeof value === 'string') { 23 - const parsedValue = stringToBoolean(value); 24 - delete obj[key]; 25 - obj[key] = parsedValue; 26 - } 27 - } 28 - return obj; 29 - }; 30 - 31 - export const runCli = async (): Promise<void> => { 32 - const params = new Command() 33 - .name(Object.keys(pkg.bin)[0]!) 34 - .usage('[options]') 35 - .version(pkg.version) 36 - .option('-c, --client <value>', 'HTTP client to generate') 37 - .option('-d, --debug', 'Set log level to debug') 38 - .option('--dry-run [value]', 'Skip writing files to disk?') 39 - .option('-f, --file [value]', 'Path to the config file') 40 - .option( 41 - '-i, --input <value>', 42 - 'OpenAPI specification (path, url, or string content)', 43 - ) 44 - .option('-l, --logs [value]', 'Logs folder') 45 - .option('-o, --output <value>', 'Output folder') 46 - .option('-p, --plugins [value...]', "List of plugins you'd like to use") 47 - .option('-s, --silent', 'Set log level to silent') 48 - .option( 49 - '--no-log-file', 50 - 'Disable writing a log file. Works like --silent but without suppressing console output', 51 - ) 52 - .option( 53 - '-w, --watch [value]', 54 - 'Regenerate the client when the input file changes?', 55 - ) 56 - .parse(process.argv) 57 - .opts(); 58 - 59 - let userConfig: Record<string, unknown>; 60 - 61 - try { 62 - userConfig = processParams(params, ['dryRun', 'logFile']); 63 - 64 - if (userConfig.file) { 65 - userConfig.configFile = userConfig.file; 66 - delete userConfig.file; 67 - } 68 - 69 - if (params.plugins === true) { 70 - userConfig.plugins = []; 71 - } else if (params.plugins) { 72 - userConfig.plugins = params.plugins; 73 - } else if (userConfig.client) { 74 - userConfig.plugins = ['@hey-api/typescript', '@hey-api/sdk']; 75 - } 76 - 77 - if (userConfig.client) { 78 - (userConfig.plugins as Array<string>).push(userConfig.client as string); 79 - delete userConfig.client; 80 - } 81 - 82 - userConfig.logs = userConfig.logs 83 - ? { 84 - path: userConfig.logs, 85 - } 86 - : {}; 87 - 88 - if (userConfig.debug) { 89 - (userConfig.logs as Record<string, unknown>).level = 'debug'; 90 - delete userConfig.debug; 91 - } else if (userConfig.silent) { 92 - (userConfig.logs as Record<string, unknown>).level = 'silent'; 93 - delete userConfig.silent; 94 - } 95 - 96 - (userConfig.logs as Record<string, unknown>).file = userConfig.logFile; 97 - delete userConfig.logFile; 98 - 99 - if (typeof params.watch === 'string') { 100 - userConfig.watch = Number.parseInt(params.watch, 10); 101 - } 102 - 103 - if (!Object.keys(userConfig.logs as Record<string, unknown>).length) { 104 - delete userConfig.logs; 105 - } 106 - 107 - const context = await createClient( 108 - userConfig as unknown as Required<Parameters<typeof createClient>>[0], 109 - ); 110 - if ( 111 - !context[0]?.config.input.some( 112 - (input) => input.watch && input.watch.enabled, 113 - ) 114 - ) { 115 - process.exit(0); 116 - } 117 - } catch { 118 - process.exit(1); 119 - } 120 - };
+40
packages/openapi-ts/src/cli/adapter.ts
··· 1 + import type { ToArray } from '@hey-api/types'; 2 + 3 + import type { UserConfig } from '~/config/types'; 4 + 5 + import type { CliOptions } from './schema'; 6 + 7 + export const cliToConfig = (cli: CliOptions): Partial<UserConfig> => { 8 + const config: Partial<UserConfig> = {}; 9 + 10 + if (cli.input) config.input = cli.input; 11 + if (cli.output) config.output = cli.output; 12 + if (cli.file) config.configFile = cli.file; 13 + if (cli.dryRun !== undefined) config.dryRun = cli.dryRun; 14 + 15 + const plugins: ToArray<UserConfig['plugins']> = []; 16 + if (cli.plugins instanceof Array && cli.plugins.length > 0) { 17 + plugins.push(...cli.plugins); 18 + } 19 + if (cli.client) plugins.push(cli.client); 20 + if (plugins.length > 0) config.plugins = plugins; 21 + 22 + if (cli.debug || cli.silent || cli.logs || cli.logFile !== undefined) { 23 + config.logs = { 24 + ...(cli.logs && { path: cli.logs }), 25 + ...(cli.debug && { level: 'debug' as const }), 26 + ...(cli.silent && { level: 'silent' as const }), 27 + ...(cli.logFile !== undefined && { file: cli.logFile }), 28 + }; 29 + } 30 + 31 + if (cli.watch !== undefined) { 32 + if (typeof cli.watch === 'string') { 33 + config.watch = Number.parseInt(cli.watch, 10); 34 + } else { 35 + config.watch = cli.watch; 36 + } 37 + } 38 + 39 + return config; 40 + };
+67
packages/openapi-ts/src/cli/index.ts
··· 1 + import { Command, CommanderError } from 'commander'; 2 + 3 + import { createClient } from '~/index'; 4 + 5 + import pkg from '../../package.json' assert { type: 'json' }; 6 + import { cliToConfig } from './adapter'; 7 + 8 + const binName = Object.keys(pkg.bin)[0]!; 9 + 10 + const program = new Command() 11 + .name(binName) 12 + .description('Generate TypeScript code from OpenAPI specifications') 13 + .version(pkg.version); 14 + 15 + program 16 + .option( 17 + '-i, --input <path...>', 18 + 'OpenAPI specification (path, URL, or string)', 19 + ) 20 + .option('-o, --output <path...>', 'Output folder(s)') 21 + .option('-c, --client <name>', 'HTTP client to generate') 22 + .option('-p, --plugins [names...]', 'Plugins to use') 23 + .option('-f, --file <path>', 'Path to config file') 24 + .option('-d, --debug', 'Enable debug logging') 25 + .option('-s, --silent', 'Suppress all output') 26 + .option('-l, --logs <path>', 'Logs folder path') 27 + .option('--no-log-file', 'Disable log file output') 28 + .option('--dry-run', 'Skip writing files') 29 + .option('-w, --watch [interval]', 'Watch for changes') 30 + .action(async (options) => { 31 + const config = cliToConfig(options); 32 + 33 + const context = await createClient( 34 + config as Parameters<typeof createClient>[0], 35 + ); 36 + 37 + const hasActiveWatch = context[0]?.config.input.some( 38 + (input) => input.watch?.enabled, 39 + ); 40 + 41 + if (!hasActiveWatch) { 42 + process.exit(0); 43 + } 44 + }); 45 + 46 + export async function runCli(): Promise<void> { 47 + try { 48 + await program.parseAsync(process.argv); 49 + } catch (error) { 50 + if (error instanceof CommanderError && 'code' in error) { 51 + if (error.code === 'commander.optionMissingArgument') { 52 + console.error( 53 + `\nMissing required argument. Run '${binName} --help' for usage.\n`, 54 + ); 55 + } else if (error.code === 'commander.unknownOption') { 56 + console.error( 57 + `\nUnknown option. Run '${binName} --help' for available options.\n`, 58 + ); 59 + } 60 + 61 + process.exit(error.exitCode); 62 + } 63 + 64 + console.error('Unexpected error:', error); 65 + process.exit(1); 66 + } 67 + }
+17
packages/openapi-ts/src/cli/schema.ts
··· 1 + import type { MaybeArray } from '@hey-api/types'; 2 + 3 + import type { PluginClientNames, PluginNames } from '~/plugins/types'; 4 + 5 + export interface CliOptions { 6 + client?: PluginClientNames; 7 + debug?: boolean; 8 + dryRun?: boolean; 9 + file?: string; 10 + input?: MaybeArray<string>; 11 + logFile?: boolean; 12 + logs?: string; 13 + output?: MaybeArray<string>; 14 + plugins?: ReadonlyArray<PluginNames>; 15 + silent?: boolean; 16 + watch?: boolean | string; 17 + }
+54
packages/openapi-ts/src/config/expand.ts
··· 1 + import colors from 'ansi-colors'; 2 + 3 + import { getInput } from './input'; 4 + import type { UserConfig } from './types'; 5 + 6 + export interface Job { 7 + config: UserConfig; 8 + index: number; 9 + } 10 + 11 + export function expandToJobs( 12 + configs: ReadonlyArray<UserConfig>, 13 + ): ReadonlyArray<Job> { 14 + const jobs: Array<Job> = []; 15 + let jobIndex = 0; 16 + 17 + for (const config of configs) { 18 + const inputs = getInput(config); 19 + const outputs = 20 + config.output instanceof Array ? config.output : [config.output]; 21 + 22 + if (outputs.length === 1) { 23 + jobs.push({ 24 + config: { 25 + ...config, 26 + input: inputs, 27 + output: outputs[0]!, // output array with single item 28 + }, 29 + index: jobIndex++, 30 + }); 31 + } else if (outputs.length > 1 && inputs.length !== outputs.length) { 32 + // Warn and create job per output (all with same inputs) 33 + console.warn( 34 + `⚙️ ${colors.yellow('Warning:')} You provided ${colors.cyan(String(inputs.length))} ${colors.cyan(inputs.length === 1 ? 'input' : 'inputs')} and ${colors.yellow(String(outputs.length))} ${colors.yellow('outputs')}. This will produce identical output in multiple locations. You likely want to provide a single output or the same number of outputs as inputs.`, 35 + ); 36 + for (const output of outputs) { 37 + jobs.push({ 38 + config: { ...config, input: inputs, output }, 39 + index: jobIndex++, 40 + }); 41 + } 42 + } else if (outputs.length > 1) { 43 + // Pair inputs with outputs by index 44 + outputs.forEach((output, index) => { 45 + jobs.push({ 46 + config: { ...config, input: inputs[index]!, output }, 47 + index: jobIndex++, 48 + }); 49 + }); 50 + } 51 + } 52 + 53 + return jobs; 54 + }
+34 -147
packages/openapi-ts/src/config/init.ts
··· 1 - import path from 'node:path'; 2 - 3 1 import type { Logger } from '@hey-api/codegen-core'; 4 - import type { ArrayOnly } from '@hey-api/types'; 5 - import colors from 'ansi-colors'; 6 - 7 - import { ConfigError } from '~/error'; 2 + import { loadConfigFile } from '@hey-api/codegen-core'; 8 3 9 - import { getInput } from './input'; 10 - import { getLogs } from './logs'; 11 - import { mergeConfigs } from './merge'; 12 - import { getOutput } from './output'; 4 + import { expandToJobs } from './expand'; 13 5 import { getProjectDependencies } from './packages'; 14 - import { getParser } from './parser'; 15 - import { getPlugins } from './plugins'; 16 - import type { Config, UserConfig } from './types'; 17 - 18 - type ConfigResult = { 19 - config: Config; 20 - errors: ReadonlyArray<Error>; 21 - jobIndex: number; 22 - }; 6 + import type { ResolvedJob } from './resolve'; 7 + import { resolveConfig } from './resolve'; 8 + import type { UserConfig } from './types'; 9 + import { validateJobs } from './validate'; 23 10 24 11 export type Configs = { 25 12 dependencies: Record<string, string>; 26 - results: ReadonlyArray<ConfigResult>; 13 + jobs: ReadonlyArray<ResolvedJob>; 14 + /** 15 + * @deprecated Use `jobs` instead. 16 + */ 17 + results: ReadonlyArray<ResolvedJob>; 27 18 }; 28 19 29 20 /** 30 - * Detect if the current session is interactive based on TTY status and environment variables. 31 - * This is used as a fallback when the user doesn't explicitly set the interactive option. 32 21 * @internal 33 22 */ 34 - export const detectInteractiveSession = (): boolean => 35 - Boolean( 36 - process.stdin.isTTY && 37 - process.stdout.isTTY && 38 - !process.env.CI && 39 - !process.env.NO_INTERACTIVE && 40 - !process.env.NO_INTERACTION, 41 - ); 42 - 43 - /** 44 - * @internal 45 - */ 46 - export const initConfigs = async ({ 23 + export async function resolveJobs({ 47 24 logger, 48 25 userConfigs, 49 26 }: { 50 27 logger: Logger; 51 28 userConfigs: ReadonlyArray<UserConfig>; 52 - }): Promise<Configs> => { 29 + }): Promise<Configs> { 53 30 const configs: Array<UserConfig> = []; 54 31 let dependencies: Record<string, string> = {}; 55 32 56 33 const eventLoad = logger.timeEvent('load'); 57 34 for (const userConfig of userConfigs) { 58 - let configurationFile: string | undefined = undefined; 59 - if (userConfig?.configFile) { 35 + let configFile: string | undefined; 36 + if (userConfig.configFile) { 60 37 const parts = userConfig.configFile.split('.'); 61 - configurationFile = parts.slice(0, parts.length - 1).join('.'); 38 + configFile = parts.slice(0, parts.length - 1).join('.'); 62 39 } 63 40 64 - const eventC12 = logger.timeEvent('c12'); 65 - // c12 is ESM-only since v3 66 - const { loadConfig } = await import('c12'); 67 - const { config: configFromFile, configFile: loadedConfigFile } = 68 - await loadConfig<UserConfig>({ 69 - configFile: configurationFile, 70 - name: 'openapi-ts', 71 - }); 72 - eventC12.timeEnd(); 41 + const loaded = await loadConfigFile<UserConfig>({ 42 + configFile, 43 + logger, 44 + name: 'openapi-ts', 45 + userConfig, 46 + }); 73 47 74 48 if (!Object.keys(dependencies).length) { 75 49 // TODO: handle dependencies for multiple configs properly? 76 50 dependencies = getProjectDependencies( 77 - Object.keys(configFromFile).length ? loadedConfigFile : undefined, 51 + loaded.foundConfig ? loaded.configFile : undefined, 78 52 ); 79 53 } 80 54 81 - const mergedConfigs = 82 - configFromFile instanceof Array 83 - ? configFromFile.map((config) => mergeConfigs(config, userConfig)) 84 - : [mergeConfigs(configFromFile, userConfig)]; 85 - 86 - for (const mergedConfig of mergedConfigs) { 87 - const input = getInput(mergedConfig); 88 - 89 - if (mergedConfig.output instanceof Array) { 90 - const countInputs = input.length; 91 - const countOutputs = mergedConfig.output.length; 92 - if (countOutputs > 1) { 93 - if (countInputs !== countOutputs) { 94 - console.warn( 95 - `⚙️ ${colors.yellow('Warning:')} You provided ${colors.cyan(String(countInputs))} ${colors.cyan(countInputs === 1 ? 'input' : 'inputs')} and ${colors.yellow(String(countOutputs))} ${colors.yellow('outputs')}. This is probably not what you want as it will produce identical output in multiple locations. You most likely want to provide a single output or the same number of outputs as inputs.`, 96 - ); 97 - for (const output of mergedConfig.output) { 98 - configs.push({ ...mergedConfig, input, output }); 99 - } 100 - } else { 101 - mergedConfig.output.forEach((output, index) => { 102 - configs.push({ ...mergedConfig, input: input[index]!, output }); 103 - }); 104 - } 105 - } else { 106 - configs.push({ 107 - ...mergedConfig, 108 - input, 109 - output: mergedConfig.output[0] ?? '', 110 - }); 111 - } 112 - } else { 113 - configs.push({ ...mergedConfig, input }); 114 - } 115 - } 55 + configs.push(...loaded.configs); 116 56 } 117 57 eventLoad.timeEnd(); 118 58 119 - const results: Array<ArrayOnly<ConfigResult>> = []; 120 - 121 59 const eventBuild = logger.timeEvent('build'); 122 - for (const userConfig of configs) { 123 - const logs = getLogs(userConfig); 124 - const input = getInput(userConfig); 125 - const output = getOutput(userConfig); 126 - const parser = getParser(userConfig); 127 - 128 - const errors: Array<Error> = []; 129 - 130 - if (!input.length) { 131 - errors.push( 132 - new ConfigError( 133 - 'missing input - which OpenAPI specification should we use to generate your output?', 134 - ), 135 - ); 136 - } 137 - 138 - if (!output.path) { 139 - errors.push( 140 - new ConfigError( 141 - 'missing output - where should we generate your output?', 142 - ), 143 - ); 144 - } 145 - 146 - output.path = path.resolve(process.cwd(), output.path); 147 - 148 - let plugins: Pick<Config, 'plugins' | 'pluginOrder'>; 149 - 150 - try { 151 - plugins = getPlugins({ dependencies, userConfig }); 152 - } catch (error) { 153 - errors.push(error); 154 - plugins = { 155 - pluginOrder: [], 156 - plugins: {}, 157 - }; 158 - } 159 - 160 - const config: Config = { 161 - configFile: userConfig.configFile ?? '', 162 - dryRun: userConfig.dryRun ?? false, 163 - input, 164 - interactive: userConfig.interactive ?? detectInteractiveSession(), 165 - logs, 166 - output, 167 - parser, 168 - pluginOrder: plugins.pluginOrder, 169 - plugins: plugins.plugins, 170 - }; 171 - 172 - const jobIndex = results.length; 173 - 174 - if (logs.level === 'debug') { 175 - const jobPrefix = colors.gray(`[Job ${jobIndex + 1}] `); 176 - console.warn(`${jobPrefix}${colors.cyan('config:')}`, config); 177 - } 178 - 179 - results.push({ config, errors, jobIndex }); 180 - } 60 + const jobs = validateJobs(expandToJobs(configs)); 61 + const resolvedJobs = jobs.map((validated) => 62 + resolveConfig(validated, dependencies), 63 + ); 181 64 eventBuild.timeEnd(); 182 65 183 - return { dependencies, results }; 184 - }; 66 + return { 67 + dependencies, 68 + jobs: resolvedJobs, 69 + results: resolvedJobs, 70 + }; 71 + }
-32
packages/openapi-ts/src/config/merge.ts
··· 1 - import type { UserConfig } from './types'; 2 - 3 - const mergeObjects = ( 4 - objA: Record<string, unknown> | undefined, 5 - objB: Record<string, unknown> | undefined, 6 - ): Record<string, unknown> => { 7 - const a = objA || {}; 8 - const b = objB || {}; 9 - return { 10 - ...a, 11 - ...b, 12 - }; 13 - }; 14 - 15 - export const mergeConfigs = ( 16 - configA: UserConfig | undefined, 17 - configB: UserConfig | undefined, 18 - ): UserConfig => { 19 - const a: Partial<UserConfig> = configA || {}; 20 - const b: Partial<UserConfig> = configB || {}; 21 - const merged: UserConfig = { 22 - ...(a as UserConfig), 23 - ...(b as UserConfig), 24 - }; 25 - if (typeof merged.logs === 'object') { 26 - merged.logs = mergeObjects( 27 - a.logs as Record<string, unknown>, 28 - b.logs as Record<string, unknown>, 29 - ); 30 - } 31 - return merged; 32 - };
+65
packages/openapi-ts/src/config/resolve.ts
··· 1 + import path from 'node:path'; 2 + 3 + import { detectInteractiveSession } from '@hey-api/codegen-core'; 4 + import colors from 'ansi-colors'; 5 + 6 + import { getInput } from './input'; 7 + import { getLogs } from './logs'; 8 + import { getOutput } from './output'; 9 + import { getParser } from './parser'; 10 + import { getPlugins } from './plugins'; 11 + import type { Config } from './types'; 12 + import type { ValidationResult } from './validate'; 13 + 14 + export type ResolvedJob = { 15 + config: Config; 16 + errors: Array<Error>; 17 + index: number; 18 + }; 19 + 20 + export const resolveConfig = ( 21 + validated: ValidationResult, 22 + dependencies: Record<string, string>, 23 + ): ResolvedJob => { 24 + const logs = getLogs(validated.job.config); 25 + const input = getInput(validated.job.config); 26 + const output = getOutput(validated.job.config); 27 + const parser = getParser(validated.job.config); 28 + 29 + output.path = path.resolve(process.cwd(), output.path); 30 + 31 + let plugins: Pick<Config, 'plugins' | 'pluginOrder'>; 32 + 33 + try { 34 + plugins = getPlugins({ dependencies, userConfig: validated.job.config }); 35 + } catch (error) { 36 + validated.errors.push(error); 37 + plugins = { 38 + pluginOrder: [], 39 + plugins: {}, 40 + }; 41 + } 42 + 43 + const config: Config = { 44 + configFile: validated.job.config.configFile ?? '', 45 + dryRun: validated.job.config.dryRun ?? false, 46 + input, 47 + interactive: validated.job.config.interactive ?? detectInteractiveSession(), 48 + logs, 49 + output, 50 + parser, 51 + pluginOrder: plugins.pluginOrder, 52 + plugins: plugins.plugins, 53 + }; 54 + 55 + if (logs.level === 'debug') { 56 + const jobPrefix = colors.gray(`[Job ${validated.job.index}] `); 57 + console.warn(`${jobPrefix}${colors.cyan('config:')}`, config); 58 + } 59 + 60 + return { 61 + config, 62 + errors: validated.errors, 63 + index: validated.job.index, 64 + }; 65 + };
+2 -2
packages/openapi-ts/src/config/types.d.ts
··· 9 9 10 10 import type { Output, UserOutput } from './output'; 11 11 12 - export interface UserConfig { 12 + export type UserConfig = { 13 13 /** 14 14 * Path to the config file. Set this value if you don't use the default 15 15 * config file name, or it's not located in the project root. ··· 81 81 * @deprecated use `input.watch` instead 82 82 */ 83 83 watch?: boolean | number | Watch; 84 - } 84 + }; 85 85 86 86 export type Config = Omit< 87 87 Required<UserConfig>,
+39
packages/openapi-ts/src/config/validate.ts
··· 1 + import { ConfigError } from '~/error'; 2 + 3 + import type { Job } from './expand'; 4 + import { getInput } from './input'; 5 + import { getOutput } from './output'; 6 + 7 + export interface ValidationResult { 8 + errors: Array<ConfigError>; 9 + job: Job; 10 + } 11 + 12 + export function validateJobs( 13 + jobs: ReadonlyArray<Job>, 14 + ): ReadonlyArray<ValidationResult> { 15 + return jobs.map((job) => { 16 + const errors: Array<ConfigError> = []; 17 + const { config } = job; 18 + 19 + const inputs = getInput(config); 20 + if (!inputs.length) { 21 + errors.push( 22 + new ConfigError( 23 + 'missing input - which OpenAPI specification should we use to generate your output?', 24 + ), 25 + ); 26 + } 27 + 28 + const output = getOutput(config); 29 + if (!output.path) { 30 + errors.push( 31 + new ConfigError( 32 + 'missing output - where should we generate your output?', 33 + ), 34 + ); 35 + } 36 + 37 + return { errors, job }; 38 + }); 39 + }
+27 -37
packages/openapi-ts/src/generate.ts
··· 3 3 4 4 import { checkNodeVersion } from '~/config/engine'; 5 5 import type { Configs } from '~/config/init'; 6 - import { initConfigs } from '~/config/init'; 6 + import { resolveJobs } from '~/config/init'; 7 7 import { getLogs } from '~/config/logs'; 8 8 import type { UserConfig } from '~/config/types'; 9 9 import { createClient as pCreateClient } from '~/createClient'; ··· 23 23 * 24 24 * @param userConfig User provided {@link UserConfig} configuration(s). 25 25 */ 26 - export const createClient = async ( 26 + export async function createClient( 27 27 userConfig?: LazyOrAsync<MaybeArray<UserConfig>>, 28 28 logger = new Logger(), 29 - ): Promise<ReadonlyArray<Context>> => { 29 + ): Promise<ReadonlyArray<Context>> { 30 30 const resolvedConfig = 31 31 typeof userConfig === 'function' ? await userConfig() : userConfig; 32 32 const userConfigs = resolvedConfig ··· 42 42 rawLogs = getLogs({ logs: rawLogs }); 43 43 } 44 44 45 - let configs: Configs | undefined; 45 + let jobs: Configs['jobs'] = []; 46 46 47 47 try { 48 48 checkNodeVersion(); ··· 50 50 const eventCreateClient = logger.timeEvent('createClient'); 51 51 52 52 const eventConfig = logger.timeEvent('config'); 53 - configs = await initConfigs({ logger, userConfigs }); 54 - const printIntro = configs.results.some( 55 - (result) => result.config.logs.level !== 'silent', 56 - ); 57 - if (printIntro) { 58 - printCliIntro(); 59 - } 53 + const resolved = await resolveJobs({ logger, userConfigs }); 54 + const dependencies = resolved.dependencies; 55 + jobs = resolved.jobs; 56 + const printIntro = jobs.some((job) => job.config.logs.level !== 'silent'); 57 + if (printIntro) printCliIntro(); 60 58 eventConfig.timeEnd(); 61 59 62 - const allConfigErrors = configs.results.flatMap((result) => 63 - result.errors.map((error) => ({ error, jobIndex: result.jobIndex })), 60 + const configErrors = jobs.flatMap((job) => 61 + job.errors.map((error) => ({ error, jobIndex: job.index })), 64 62 ); 65 - if (allConfigErrors.length) { 66 - throw new ConfigValidationError(allConfigErrors); 63 + if (configErrors.length > 0) { 64 + throw new ConfigValidationError(configErrors); 67 65 } 68 66 69 - const clients = await Promise.all( 70 - configs.results.map(async (result) => { 67 + const outputs = await Promise.all( 68 + jobs.map(async (job) => { 71 69 try { 72 70 return await pCreateClient({ 73 - config: result.config, 74 - dependencies: configs!.dependencies, 75 - jobIndex: result.jobIndex, 71 + config: job.config, 72 + dependencies, 73 + jobIndex: job.index, 76 74 logger, 77 75 }); 78 76 } catch (error) { 79 77 throw new JobError('', { 80 78 error, 81 - jobIndex: result.jobIndex, 79 + jobIndex: job.index, 82 80 }); 83 81 } 84 82 }), 85 83 ); 86 - const result = clients.filter((client) => 87 - Boolean(client), 88 - ) as ReadonlyArray<Context>; 84 + const contexts = outputs.filter((ctx): ctx is Context => ctx !== undefined); 89 85 90 86 eventCreateClient.timeEnd(); 91 87 92 - const printLogs = configs.results.some( 93 - (result) => result.config.logs.level === 'debug', 94 - ); 95 - logger.report(printLogs); 88 + logger.report(jobs.some((job) => job.config.logs.level === 'debug')); 96 89 97 - return result; 90 + return contexts; 98 91 } catch (error) { 99 - const results = configs?.results ?? []; 100 - 101 92 const logs = 102 - results.find((result) => result.config.logs.level !== 'silent')?.config 103 - .logs ?? 104 - results[0]?.config.logs ?? 93 + jobs.find((job) => job.config.logs.level !== 'silent')?.config.logs ?? 94 + jobs[0]?.config.logs ?? 105 95 rawLogs; 106 96 const dryRun = 107 - results.some((result) => result.config.dryRun) ?? 97 + jobs.some((job) => job.config.dryRun) ?? 108 98 userConfigs.some((config) => config.dryRun) ?? 109 99 false; 110 100 const logPath = ··· 114 104 if (!logs || logs.level !== 'silent') { 115 105 printCrashReport({ error, logPath }); 116 106 const isInteractive = 117 - results.some((result) => result.config.interactive) ?? 107 + jobs.some((job) => job.config.interactive) ?? 118 108 userConfigs.some((config) => config.interactive) ?? 119 109 false; 120 110 if (await shouldReportCrash({ error, isInteractive })) { ··· 124 114 125 115 throw error; 126 116 } 127 - }; 117 + }
+5 -3
packages/openapi-ts/src/index.ts
··· 79 79 export { createClient } from '~/generate'; 80 80 81 81 /** 82 - * Type helper for openapi-ts.config.ts, returns {@link MaybeArray<UserConfig>} object(s) 82 + * Type helper for configuration object, returns {@link MaybeArray<UserConfig>} object(s) 83 83 */ 84 - export const defineConfig = async <T extends MaybeArray<UserConfig>>( 84 + export async function defineConfig<T extends MaybeArray<UserConfig>>( 85 85 config: LazyOrAsync<T>, 86 - ): Promise<T> => (typeof config === 'function' ? await config() : config); 86 + ): Promise<T> { 87 + return typeof config === 'function' ? await config() : config; 88 + } 87 89 88 90 export { Logger } from '@hey-api/codegen-core'; 89 91 export { defaultPaginationKeywords } from '~/config/parser';
+1 -1
packages/openapi-ts/src/internal.ts
··· 1 - export { initConfigs } from './config/init'; 1 + export { resolveJobs as initConfigs } from './config/init'; 2 2 export { getSpec } from './getSpec'; 3 3 export { parseOpenApiSpec } from './openApi';
+18 -2
packages/types/src/index.ts
··· 1 + /** 2 + * An object with string keys and unknown values. 3 + */ 4 + export type AnyObject = Record<string, unknown>; 5 + 1 6 /** 2 7 * Converts all top-level ReadonlyArray properties to Array (shallow). 3 8 */ 4 9 export type ArrayOnly<T> = { 5 - [K in keyof T]: T[K] extends ReadonlyArray<infer U> ? Array<U> : T[K]; 10 + [K in keyof T]: ToArray<T[K]>; 6 11 }; 7 12 8 13 /** ··· 42 47 * Converts all top-level Array properties to ReadonlyArray (shallow). 43 48 */ 44 49 export type ReadonlyArrayOnly<T> = { 45 - [K in keyof T]: T[K] extends Array<infer U> ? ReadonlyArray<U> : T[K]; 50 + [K in keyof T]: ToReadonlyArray<T[K]>; 46 51 }; 52 + 53 + /** 54 + * Converts ReadonlyArray<T> to Array<T>, preserving unions. 55 + */ 56 + export type ToArray<T> = T extends ReadonlyArray<infer U> ? Array<U> : T; 57 + 58 + /** 59 + * Converts Array<T> to ReadonlyArray<T>, preserving unions. 60 + */ 61 + export type ToReadonlyArray<T> = 62 + T extends ReadonlyArray<infer U> ? ReadonlyArray<U> : T;
+11 -39
pnpm-lock.yaml
··· 1253 1253 ansi-colors: 1254 1254 specifier: 4.1.3 1255 1255 version: 4.1.3 1256 + c12: 1257 + specifier: 3.3.3 1258 + version: 3.3.3(magicast@0.3.5) 1256 1259 color-support: 1257 1260 specifier: 1.1.3 1258 1261 version: 1.1.3 ··· 1346 1349 ansi-colors: 1347 1350 specifier: 4.1.3 1348 1351 version: 4.1.3 1349 - c12: 1350 - specifier: 3.3.3 1351 - version: 3.3.3(magicast@0.3.5) 1352 1352 color-support: 1353 1353 specifier: 1.1.3 1354 1354 version: 1.1.3 ··· 1401 1401 ansi-colors: 1402 1402 specifier: 4.1.3 1403 1403 version: 4.1.3 1404 - c12: 1405 - specifier: 3.3.3 1406 - version: 3.3.3(magicast@0.3.5) 1407 1404 color-support: 1408 1405 specifier: 1.1.3 1409 1406 version: 1.1.3 ··· 7904 7901 resolution: {integrity: sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A==} 7905 7902 peerDependencies: 7906 7903 magicast: ^0.3.5 7907 - peerDependenciesMeta: 7908 - magicast: 7909 - optional: true 7910 - 7911 - c12@3.3.2: 7912 - resolution: {integrity: sha512-QkikB2X5voO1okL3QsES0N690Sn/K9WokXqUsDQsWy5SnYb+psYQFGA10iy1bZHj3fjISKsI67Q90gruvWWM3A==} 7913 - peerDependencies: 7914 - magicast: '*' 7915 7904 peerDependenciesMeta: 7916 7905 magicast: 7917 7906 optional: true ··· 14693 14682 '@vitejs/plugin-basic-ssl': 1.2.0(vite@7.2.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.2)) 14694 14683 ansi-colors: 4.1.3 14695 14684 autoprefixer: 10.4.20(postcss@8.5.2) 14696 - babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0) 14685 + babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)) 14697 14686 browserslist: 4.25.4 14698 14687 copy-webpack-plugin: 12.0.2(webpack@5.98.0) 14699 14688 css-loader: 7.1.2(webpack@5.98.0) ··· 14713 14702 picomatch: 4.0.2 14714 14703 piscina: 4.8.0 14715 14704 postcss: 8.5.2 14716 - postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0) 14705 + postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) 14717 14706 resolve-url-loader: 5.0.0 14718 14707 rxjs: 7.8.1 14719 14708 sass: 1.85.0 ··· 14781 14770 '@vitejs/plugin-basic-ssl': 1.2.0(vite@7.2.2(@types/node@22.10.5)(jiti@2.6.1)(less@4.2.2)(sass@1.85.0)(terser@5.39.0)(yaml@2.8.2)) 14782 14771 ansi-colors: 4.1.3 14783 14772 autoprefixer: 10.4.20(postcss@8.5.2) 14784 - babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0) 14773 + babel-loader: 9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)) 14785 14774 browserslist: 4.25.4 14786 14775 copy-webpack-plugin: 12.0.2(webpack@5.98.0) 14787 14776 css-loader: 7.1.2(webpack@5.98.0) ··· 14801 14790 picomatch: 4.0.2 14802 14791 piscina: 4.8.0 14803 14792 postcss: 8.5.2 14804 - postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0) 14793 + postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) 14805 14794 resolve-url-loader: 5.0.0 14806 14795 rxjs: 7.8.1 14807 14796 sass: 1.85.0 ··· 14889 14878 picomatch: 4.0.2 14890 14879 piscina: 4.8.0 14891 14880 postcss: 8.5.2 14892 - postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0) 14881 + postcss-loader: 8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)) 14893 14882 resolve-url-loader: 5.0.0 14894 14883 rxjs: 7.8.1 14895 14884 sass: 1.85.0 ··· 19166 19155 '@nuxt/test-utils@3.21.0(@vue/test-utils@2.4.6)(jsdom@23.0.0)(magicast@0.3.5)(typescript@5.9.3)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.10.5)(jiti@2.6.1)(jsdom@23.0.0)(less@4.2.2)(sass@1.85.0)(terser@5.43.1)(yaml@2.8.2))': 19167 19156 dependencies: 19168 19157 '@nuxt/kit': 3.20.2(magicast@0.3.5) 19169 - c12: 3.3.2(magicast@0.3.5) 19158 + c12: 3.3.3(magicast@0.3.5) 19170 19159 consola: 3.4.2 19171 19160 defu: 6.1.4 19172 19161 destr: 2.0.5 ··· 22379 22368 schema-utils: 4.3.2 22380 22369 webpack: 5.98.0(esbuild@0.25.0) 22381 22370 22382 - babel-loader@9.2.1(@babel/core@7.26.9)(webpack@5.98.0): 22371 + babel-loader@9.2.1(@babel/core@7.26.9)(webpack@5.98.0(esbuild@0.25.0)): 22383 22372 dependencies: 22384 22373 '@babel/core': 7.26.9 22385 22374 find-cache-dir: 4.0.0 ··· 22596 22585 optionalDependencies: 22597 22586 magicast: 0.3.5 22598 22587 22599 - c12@3.3.2(magicast@0.3.5): 22600 - dependencies: 22601 - chokidar: 4.0.3 22602 - confbox: 0.2.2 22603 - defu: 6.1.4 22604 - dotenv: 17.2.3 22605 - exsolve: 1.0.8 22606 - giget: 2.0.0 22607 - jiti: 2.6.1 22608 - ohash: 2.0.11 22609 - pathe: 2.0.3 22610 - perfect-debounce: 2.0.0 22611 - pkg-types: 2.3.0 22612 - rc9: 2.1.2 22613 - optionalDependencies: 22614 - magicast: 0.3.5 22615 - 22616 22588 c12@3.3.3(magicast@0.3.5): 22617 22589 dependencies: 22618 22590 chokidar: 5.0.0 ··· 27869 27841 ts-node: 10.9.2(@types/node@22.10.5)(typescript@5.9.3) 27870 27842 optional: true 27871 27843 27872 - postcss-loader@8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0): 27844 + postcss-loader@8.1.1(postcss@8.5.2)(typescript@5.8.3)(webpack@5.98.0(esbuild@0.25.0)): 27873 27845 dependencies: 27874 27846 cosmiconfig: 9.0.0(typescript@5.8.3) 27875 27847 jiti: 1.21.7