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.

chore: update tests, docs and address reviews

+227 -172
+34
docs/openapi-ts/configuration.md
··· 38 38 39 39 Alternatively, you can use `openapi-ts.config.js` and configure the export statement depending on your project setup. 40 40 41 + ### Async config and factories 42 + 43 + You can also export a function (sync or async) if you need to compute configuration dynamically (e.g., read env vars): 44 + 45 + ```js 46 + import { defineConfig } from '@hey-api/openapi-ts'; 47 + 48 + export default defineConfig(async () => { 49 + return { 50 + input: 'hey-api/backend', 51 + output: 'src/client', 52 + }; 53 + }); 54 + ``` 55 + 56 + ### Multiple configurations 57 + 58 + You can also export an array to run multiple configurations in a single invocation (e.g., generate multiple clients): 59 + 60 + ```js 61 + import { defineConfig } from '@hey-api/openapi-ts'; 62 + 63 + export default defineConfig([ 64 + { 65 + input: 'path/to/openapi_one.json', 66 + output: 'src/client_one', 67 + }, 68 + { 69 + input: 'path/to/openapi_two.json', 70 + output: 'src/client_two', 71 + }, 72 + ]); 73 + ``` 74 + 41 75 <!-- 42 76 TODO: uncomment after c12 supports multiple configs 43 77 see https://github.com/unjs/c12/issues/92
+15 -1
docs/openapi-ts/configuration/input.md
··· 9 9 10 10 ## Input 11 11 12 - The input can be a string path, URL, [API registry](#api-registry), an object containing any of these, or an object representing an OpenAPI specification. Hey API supports all valid OpenAPI versions and file formats. 12 + The input can be a string path, URL, [API registry](#api-registry), an object containing any of these, or an object representing an OpenAPI specification. You can also pass an array of inputs to merge multiple specifications. Hey API supports all valid OpenAPI versions and file formats. 13 13 14 14 ::: code-group 15 15 ··· 51 51 }; 52 52 ``` 53 53 <!-- prettier-ignore-end --> 54 + 55 + ```js [array] 56 + export default { 57 + input: [ 58 + // [!code ++] 59 + 'hey-api/backend', // [!code ++] 60 + './overrides/openapi.yaml', // [!code ++] 61 + ], // [!code ++] 62 + }; 63 + // When you pass multiple inputs as an array, `@hey-api/openapi-ts` bundles them into a single resolved OpenAPI 64 + // document. To avoid name collisions between files, component names may be prefixed with the input file’s base 65 + // name when needed (for example, `users.yaml` ➜ `users.*`). References across files are resolved, and 66 + // later inputs in the array can override earlier ones on conflict. 67 + ``` 54 68 55 69 ::: 56 70
+14
docs/openapi-ts/configuration/output.md
··· 32 32 ``` 33 33 <!-- prettier-ignore-end --> 34 34 35 + ```js [array] 36 + export default { 37 + input: 'hey-api/backend', // sign up at app.heyapi.dev 38 + output: [ 39 + // [!code ++] 40 + 'src/client', // [!code ++] 41 + { path: 'src/client-formatted', format: 'prettier' }, // [!code ++] 42 + { path: 'src/client-linted', lint: 'eslint', clean: false }, // [!code ++] 43 + ], // [!code ++] 44 + }; 45 + ``` 46 + 47 + <!-- prettier-ignore-end --> 48 + 35 49 ::: 36 50 37 51 ## Format
+4 -4
packages/openapi-ts-tests/main/test/2.0.x.test.ts
··· 21 21 version, 22 22 typeof userConfig.input === 'string' 23 23 ? userConfig.input 24 - : Array.isArray(userConfig.input) 24 + : userConfig.input instanceof Array 25 25 ? (userConfig.input[0] as any).path || userConfig.input[0] 26 26 : (userConfig.input as any).path, 27 27 ); ··· 402 402 const outputPath = 403 403 typeof config.output === 'string' 404 404 ? config.output 405 - : Array.isArray(config.output) 405 + : config.output instanceof Array 406 406 ? typeof config.output[0] === 'string' 407 407 ? config.output[0] 408 - : (config.output[0] as any).path 409 - : (config.output as any).path; 408 + : config.output[0]?.path || '' 409 + : config.output.path; 410 410 const filePaths = getFilePaths(outputPath); 411 411 412 412 await Promise.all(
+4 -4
packages/openapi-ts-tests/main/test/3.0.x.test.ts
··· 21 21 version, 22 22 typeof userConfig.input === 'string' 23 23 ? userConfig.input 24 - : Array.isArray(userConfig.input) 24 + : userConfig.input instanceof Array 25 25 ? (userConfig.input[0] as any).path || userConfig.input[0] 26 26 : (userConfig.input as any).path, 27 27 ); ··· 666 666 const outputPath = 667 667 typeof config.output === 'string' 668 668 ? config.output 669 - : Array.isArray(config.output) 669 + : config.output instanceof Array 670 670 ? typeof config.output[0] === 'string' 671 671 ? config.output[0] 672 - : (config.output[0] as any).path 673 - : (config.output as any).path; 672 + : config.output[0]?.path || '' 673 + : config.output.path; 674 674 const filePaths = getFilePaths(outputPath); 675 675 676 676 await Promise.all(
+4 -4
packages/openapi-ts-tests/main/test/3.1.x.test.ts
··· 21 21 version, 22 22 typeof userConfig.input === 'string' 23 23 ? userConfig.input 24 - : Array.isArray(userConfig.input) 24 + : userConfig.input instanceof Array 25 25 ? (userConfig.input[0] as any).path || userConfig.input[0] 26 26 : (userConfig.input as any).path, 27 27 ); ··· 965 965 const outputPath = 966 966 typeof config.output === 'string' 967 967 ? config.output 968 - : Array.isArray(config.output) 968 + : config.output instanceof Array 969 969 ? typeof config.output[0] === 'string' 970 970 ? config.output[0] 971 - : (config.output[0] as any).path 972 - : (config.output as any).path; 971 + : config.output[0]?.path || '' 972 + : config.output.path; 973 973 const filePaths = getFilePaths(outputPath); 974 974 975 975 await Promise.all(
+9 -9
packages/openapi-ts-tests/main/test/clients.test.ts
··· 178 178 const outputPath = 179 179 typeof config.output === 'string' 180 180 ? config.output 181 - : Array.isArray(config.output) 181 + : config.output instanceof Array 182 182 ? typeof config.output[0] === 'string' 183 183 ? config.output[0] 184 - : (config.output[0] as any)?.path || '' 185 - : (config.output as any).path; 184 + : config.output[0]?.path || '' 185 + : config.output.path; 186 186 const filePaths = getFilePaths(outputPath); 187 187 188 188 await Promise.all( ··· 341 341 const outputPath = 342 342 typeof config.output === 'string' 343 343 ? config.output 344 - : Array.isArray(config.output) 344 + : config.output instanceof Array 345 345 ? typeof config.output[0] === 'string' 346 346 ? config.output[0] 347 - : (config.output[0] as any)?.path || '' 348 - : (config.output as any).path; 347 + : config.output[0]?.path || '' 348 + : config.output.path; 349 349 const filePaths = getFilePaths(outputPath); 350 350 351 351 await Promise.all( ··· 493 493 const outputPath = 494 494 typeof config.output === 'string' 495 495 ? config.output 496 - : Array.isArray(config.output) 496 + : config.output instanceof Array 497 497 ? typeof config.output[0] === 'string' 498 498 ? config.output[0] 499 - : (config.output[0] as any)?.path || '' 500 - : (config.output as any).path; 499 + : config.output[0]?.path || '' 500 + : config.output.path; 501 501 const filePaths = getFilePaths(outputPath); 502 502 503 503 await Promise.all(
+3 -3
packages/openapi-ts-tests/main/test/plugins.test.ts
··· 569 569 const outputPath = 570 570 typeof config.output === 'string' 571 571 ? config.output 572 - : Array.isArray(config.output) 572 + : config.output instanceof Array 573 573 ? typeof config.output[0] === 'string' 574 574 ? config.output[0] 575 - : (config.output[0] as any)?.path || '' 576 - : (config.output as any).path; 575 + : config.output[0]?.path || '' 576 + : config.output.path; 577 577 const filePaths = getFilePaths(outputPath); 578 578 579 579 await Promise.all(
+1 -1
packages/openapi-ts-tests/zod/v3/test/3.0.x.test.ts
··· 64 64 const outputPath = 65 65 typeof config.output === 'string' 66 66 ? config.output 67 - : Array.isArray(config.output) 67 + : config.output instanceof Array 68 68 ? typeof config.output[0] === 'string' 69 69 ? config.output[0] 70 70 : config.output[0]?.path || ''
+1 -1
packages/openapi-ts-tests/zod/v3/test/3.1.x.test.ts
··· 146 146 const outputPath = 147 147 typeof config.output === 'string' 148 148 ? config.output 149 - : Array.isArray(config.output) 149 + : config.output instanceof Array 150 150 ? typeof config.output[0] === 'string' 151 151 ? config.output[0] 152 152 : config.output[0]?.path || ''
+1 -1
packages/openapi-ts-tests/zod/v3/test/openapi.test.ts
··· 57 57 const outputPath = 58 58 typeof config.output === 'string' 59 59 ? config.output 60 - : Array.isArray(config.output) 60 + : config.output instanceof Array 61 61 ? typeof config.output[0] === 'string' 62 62 ? config.output[0] 63 63 : config.output[0]?.path || ''
+1 -1
packages/openapi-ts-tests/zod/v3/test/utils.ts
··· 24 24 openApiVersion, 25 25 typeof userConfig.input === 'string' 26 26 ? userConfig.input 27 - : Array.isArray(userConfig.input) 27 + : userConfig.input instanceof Array 28 28 ? typeof userConfig.input[0] === 'string' 29 29 ? (userConfig.input[0] as string) 30 30 : (userConfig.input[0] as any)?.path || ''
+1 -1
packages/openapi-ts-tests/zod/v4/test/3.0.x.test.ts
··· 64 64 const outputPath = 65 65 typeof config.output === 'string' 66 66 ? config.output 67 - : Array.isArray(config.output) 67 + : config.output instanceof Array 68 68 ? typeof config.output[0] === 'string' 69 69 ? config.output[0] 70 70 : config.output[0]?.path || ''
+1 -1
packages/openapi-ts-tests/zod/v4/test/3.1.x.test.ts
··· 146 146 const outputPath = 147 147 typeof config.output === 'string' 148 148 ? config.output 149 - : Array.isArray(config.output) 149 + : config.output instanceof Array 150 150 ? typeof config.output[0] === 'string' 151 151 ? config.output[0] 152 152 : config.output[0]?.path || ''
+1 -1
packages/openapi-ts-tests/zod/v4/test/openapi.test.ts
··· 57 57 const outputPath = 58 58 typeof config.output === 'string' 59 59 ? config.output 60 - : Array.isArray(config.output) 60 + : config.output instanceof Array 61 61 ? typeof config.output[0] === 'string' 62 62 ? config.output[0] 63 63 : config.output[0]?.path || ''
+1 -1
packages/openapi-ts-tests/zod/v4/test/utils.ts
··· 24 24 openApiVersion, 25 25 typeof userConfig.input === 'string' 26 26 ? userConfig.input 27 - : Array.isArray(userConfig.input) 27 + : userConfig.input instanceof Array 28 28 ? typeof userConfig.input[0] === 'string' 29 29 ? (userConfig.input[0] as string) 30 30 : (userConfig.input[0] as any)?.path || ''
+119 -114
packages/openapi-ts/src/__tests__/index.test.ts
··· 1 + import { platform } from 'os'; 1 2 import { describe, expect, it } from 'vitest'; 2 3 3 4 import type { UserConfig } from '../types/config'; 4 5 5 - describe('main entry index', () => { 6 - describe('createClient', () => { 7 - it('should be exported', async () => { 8 - const { createClient } = await import('../index'); 9 - expect(createClient).toBeDefined(); 10 - }); 6 + describe( 7 + 'main entry index', 8 + () => { 9 + describe('createClient', () => { 10 + it('should be exported', async () => { 11 + const { createClient } = await import('../index'); 12 + expect(createClient).toBeDefined(); 13 + }); 11 14 12 - it('should handle single output configuration', async () => { 13 - const { createClient } = await import('../index'); 15 + it('should handle single output configuration', async () => { 16 + const { createClient } = await import('../index'); 14 17 15 - const config: UserConfig = { 16 - dryRun: true, 17 - input: { 18 - info: { title: 'Test API', version: '1.0.0' }, 19 - openapi: '3.0.0', 20 - paths: {}, 21 - }, 22 - output: 'test-output', 23 - plugins: ['@hey-api/typescript'], 24 - }; 18 + const config: UserConfig = { 19 + dryRun: true, 20 + input: { 21 + info: { title: 'Test API', version: '1.0.0' }, 22 + openapi: '3.0.0', 23 + paths: {}, 24 + }, 25 + output: 'test-output', 26 + plugins: ['@hey-api/typescript'], 27 + }; 25 28 26 - const results = await createClient(config); 27 - expect(results).toHaveLength(1); 28 - }); 29 + const results = await createClient(config); 30 + expect(results).toHaveLength(1); 31 + }); 29 32 30 - it('should handle multiple string outputs', async () => { 31 - const { createClient } = await import('../index'); 33 + it('should handle multiple string outputs', async () => { 34 + const { createClient } = await import('../index'); 32 35 33 - const config: UserConfig = { 34 - dryRun: true, 35 - input: { 36 - info: { title: 'Test API', version: '1.0.0' }, 37 - openapi: '3.0.0', 38 - paths: {}, 39 - }, 40 - output: ['test-output-1', 'test-output-2', 'test-output-3'], 41 - plugins: ['@hey-api/typescript'], 42 - }; 36 + const config: UserConfig = { 37 + dryRun: true, 38 + input: { 39 + info: { title: 'Test API', version: '1.0.0' }, 40 + openapi: '3.0.0', 41 + paths: {}, 42 + }, 43 + output: ['test-output-1', 'test-output-2', 'test-output-3'], 44 + plugins: ['@hey-api/typescript'], 45 + }; 43 46 44 - const results = await createClient(config); 45 - expect(results).toHaveLength(3); 46 - }); 47 + const results = await createClient(config); 48 + expect(results).toHaveLength(3); 49 + }); 47 50 48 - it('should handle multiple output objects with different configurations', async () => { 49 - const { createClient } = await import('../index'); 51 + it('should handle multiple output objects with different configurations', async () => { 52 + const { createClient } = await import('../index'); 50 53 51 - const config: UserConfig = { 52 - dryRun: true, 53 - input: { 54 - info: { title: 'Test API', version: '1.0.0' }, 55 - openapi: '3.0.0', 56 - paths: {}, 57 - }, 58 - output: [ 59 - { 60 - clean: true, 61 - format: 'prettier', 62 - path: 'test-output-formatted', 54 + const config: UserConfig = { 55 + dryRun: true, 56 + input: { 57 + info: { title: 'Test API', version: '1.0.0' }, 58 + openapi: '3.0.0', 59 + paths: {}, 63 60 }, 64 - { 65 - clean: false, 66 - lint: 'eslint', 67 - path: 'test-output-linted', 68 - }, 69 - ], 70 - plugins: ['@hey-api/typescript'], 71 - }; 61 + output: [ 62 + { 63 + clean: true, 64 + format: 'prettier', 65 + path: 'test-output-formatted', 66 + }, 67 + { 68 + clean: false, 69 + lint: 'eslint', 70 + path: 'test-output-linted', 71 + }, 72 + ], 73 + plugins: ['@hey-api/typescript'], 74 + }; 72 75 73 - const results = await createClient(config); 74 - expect(results).toHaveLength(2); 75 - }); 76 + const results = await createClient(config); 77 + expect(results).toHaveLength(2); 78 + }); 76 79 77 - it('should handle mixed string and object outputs', async () => { 78 - const { createClient } = await import('../index'); 80 + it('should handle mixed string and object outputs', async () => { 81 + const { createClient } = await import('../index'); 79 82 80 - const config: UserConfig = { 81 - dryRun: true, 82 - input: { 83 - info: { title: 'Test API', version: '1.0.0' }, 84 - openapi: '3.0.0', 85 - paths: {}, 86 - }, 87 - output: [ 88 - 'test-simple-output', 89 - { 90 - format: 'prettier', 91 - indexFile: false, 92 - path: 'test-advanced-output', 83 + const config: UserConfig = { 84 + dryRun: true, 85 + input: { 86 + info: { title: 'Test API', version: '1.0.0' }, 87 + openapi: '3.0.0', 88 + paths: {}, 93 89 }, 94 - ], 95 - plugins: ['@hey-api/typescript'], 96 - }; 90 + output: [ 91 + 'test-simple-output', 92 + { 93 + format: 'prettier', 94 + indexFile: false, 95 + path: 'test-advanced-output', 96 + }, 97 + ], 98 + plugins: ['@hey-api/typescript'], 99 + }; 97 100 98 - const results = await createClient(config); 99 - expect(results).toHaveLength(2); 100 - }); 101 + const results = await createClient(config); 102 + expect(results).toHaveLength(2); 103 + }); 101 104 102 - it('should preserve global config across multiple outputs', async () => { 103 - const { createClient } = await import('../index'); 105 + it('should preserve global config across multiple outputs', async () => { 106 + const { createClient } = await import('../index'); 104 107 105 - const config: UserConfig = { 106 - dryRun: true, 107 - experimentalParser: true, 108 - input: { 109 - info: { title: 'Test API', version: '1.0.0' }, 110 - openapi: '3.0.0', 111 - paths: { 112 - '/test': { 113 - get: { 114 - responses: { 115 - '200': { 116 - content: { 117 - 'application/json': { 118 - schema: { type: 'string' }, 108 + const config: UserConfig = { 109 + dryRun: true, 110 + experimentalParser: true, 111 + input: { 112 + info: { title: 'Test API', version: '1.0.0' }, 113 + openapi: '3.0.0', 114 + paths: { 115 + '/test': { 116 + get: { 117 + responses: { 118 + '200': { 119 + content: { 120 + 'application/json': { 121 + schema: { type: 'string' }, 122 + }, 119 123 }, 124 + description: 'Success', 120 125 }, 121 - description: 'Success', 122 126 }, 123 127 }, 124 128 }, 125 129 }, 126 130 }, 127 - }, 128 - output: ['output-1', 'output-2'], 129 - plugins: ['@hey-api/typescript'], 130 - }; 131 + output: ['output-1', 'output-2'], 132 + plugins: ['@hey-api/typescript'], 133 + }; 131 134 132 - const results = await createClient(config); 133 - expect(results).toHaveLength(2); 135 + const results = await createClient(config); 136 + expect(results).toHaveLength(2); 134 137 135 - // Both results should be IR.Context objects (due to experimentalParser: true) 136 - // and should have the same input specification 137 - results.forEach((result) => { 138 - if ('spec' in result) { 139 - expect(result.spec.info.title).toBe('Test API'); 140 - expect(result.config.experimentalParser).toBe(true); 141 - } 138 + // Both results should be IR.Context objects (due to experimentalParser: true) 139 + // and should have the same input specification 140 + results.forEach((result) => { 141 + if ('spec' in result) { 142 + expect(result.spec.info.title).toBe('Test API'); 143 + expect(result.config.experimentalParser).toBe(true); 144 + } 145 + }); 142 146 }); 143 147 }); 144 - }); 145 - }); 148 + }, 149 + { timeout: platform() === 'win32' ? 12500 : 7500 }, 150 + );
-17
packages/openapi-ts/src/config/init.ts
··· 61 61 } 62 62 } 63 63 64 - // const { config: configFromFile, sources } = await loadConfig< 65 - // UserConfig | ReadonlyArray<UserConfig> 66 - // >({ 67 - // merge: false, 68 - // sources: [ 69 - // { 70 - // extensions: ['ts', 'mts', 'cts', 'js', 'mjs', 'cjs', 'json', ''], 71 - // files: configurationFile ?? 'openapi-ts.config', 72 - // }, 73 - // ], 74 - // }); 75 64 const { config: configFromFile, configFile: loadedConfigFile } = 76 65 await loadConfig<UserConfig>({ 77 66 configFile: configurationFile, 78 67 name: 'openapi-ts', 79 68 }); 80 - 81 - // const loadedConfigFile = Array.isArray(sources) 82 - // ? (sources as ReadonlyArray<{ config?: unknown; filepath?: string }>).find( 83 - // (s) => s.config != null, 84 - // )?.filepath 85 - // : undefined; 86 69 87 70 const dependencies = getProjectDependencies( 88 71 Object.keys(configFromFile).length ? loadedConfigFile : undefined,
+1 -1
packages/openapi-ts/src/config/input.ts
··· 44 44 input.path = userConfig.input; 45 45 } else if ( 46 46 userConfig.input && 47 - !Array.isArray(userConfig.input) && 47 + !(userConfig.input instanceof Array) && 48 48 (userConfig.input.path !== undefined || 49 49 userConfig.input.organization !== undefined) 50 50 ) {
+10 -5
packages/openapi-ts/src/index.ts
··· 19 19 import { Logger } from './utils/logger'; 20 20 21 21 type ConfigValue = UserConfig | ReadonlyArray<UserConfig>; 22 - type Configs = ConfigValue | (() => ConfigValue) | (() => Promise<ConfigValue>); 22 + // Generic input shape for config that may be a value or a (possibly async) factory 23 + type ConfigInput<T extends ConfigValue> = T | (() => T) | (() => Promise<T>); 23 24 24 25 colors.enabled = colorSupport().hasBasic; 25 26 ··· 29 30 * @param userConfig User provided {@link UserConfig} configuration. 30 31 */ 31 32 export const createClient = async ( 32 - userConfig?: Configs, 33 + userConfig?: ConfigInput<ConfigValue>, 33 34 logger = new Logger(), 34 35 ): Promise<ReadonlyArray<Client | IR.Context>> => { 35 36 const resolvedConfig = ··· 105 106 }; 106 107 107 108 /** 108 - * Type helper for openapi-ts.config.ts, returns {@link UserConfig} object 109 + * Type helper for openapi-ts.config.ts, preserves input shape (object vs array) 109 110 */ 110 - export const defineConfig = async (config: Configs): Promise<ConfigValue> => 111 - typeof config === 'function' ? await config() : config; 111 + export const defineConfig = async <T extends ConfigValue>( 112 + config: ConfigInput<T>, 113 + ): Promise<T> => 114 + typeof config === 'function' 115 + ? await (config as () => T | Promise<T>)() 116 + : (config as T); 112 117 113 118 export { defaultPaginationKeywords } from './config/parser'; 114 119 export { defaultPlugins } from './config/plugins';
+2 -2
packages/openapi-ts/src/types/config.d.ts
··· 28 28 * 29 29 * Alternatively, you can define a configuration object with more options. 30 30 */ 31 - input: InputPath | Input | (InputPath | Input)[]; 31 + input: InputPath | Input | ReadonlyArray<InputPath | Input>; 32 32 /** 33 33 * Show an interactive error reporting tool when the program crashes? You 34 34 * generally want to keep this disabled (default). ··· 47 47 * outputs to generate different versions of your SDK with different 48 48 * configurations (e.g., different plugins, formatters, or paths). 49 49 */ 50 - output: string | Output | (string | Output)[]; 50 + output: string | Output | ReadonlyArray<string | Output>; 51 51 /** 52 52 * Customize how the input is parsed and transformed before it's passed to 53 53 * plugins.