Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/dry-jeans-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@hey-api/openapi-ts': minor
'@hey-api/custom-client': patch
'@hey-api/codegen-core': patch
'@hey-api/vite-plugin': patch
'@hey-api/nuxt': patch
'@docs/openapi-ts': patch
---

feat: expand configuration by allowing multiple configs, inputs and outputs
34 changes: 34 additions & 0 deletions docs/openapi-ts/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,40 @@ export default {

Alternatively, you can use `openapi-ts.config.js` and configure the export statement depending on your project setup.

### Async config and factories

You can also export a function (sync or async) if you need to compute configuration dynamically (e.g., read env vars):

```js
import { defineConfig } from '@hey-api/openapi-ts';

export default defineConfig(async () => {
return {
input: 'hey-api/backend',
output: 'src/client',
};
});
```

### Multiple configurations

You can also export an array to run multiple configurations in a single invocation (e.g., generate multiple clients):

```js
import { defineConfig } from '@hey-api/openapi-ts';

export default defineConfig([
{
input: 'path/to/openapi_one.json',
output: 'src/client_one',
},
{
input: 'path/to/openapi_two.json',
output: 'src/client_two',
},
]);
```

<!--
TODO: uncomment after c12 supports multiple configs
see https://github.com/unjs/c12/issues/92
Expand Down
16 changes: 15 additions & 1 deletion docs/openapi-ts/configuration/input.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ You must provide an input so we can load your OpenAPI specification.

## Input

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.
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.

::: code-group

Expand Down Expand Up @@ -52,6 +52,20 @@ export default {
```
<!-- prettier-ignore-end -->

```js [array]
export default {
input: [
// [!code ++]
'hey-api/backend', // [!code ++]
'./overrides/openapi.yaml', // [!code ++]
], // [!code ++]
};
// When you pass multiple inputs as an array, `@hey-api/openapi-ts` bundles them into a single resolved OpenAPI
// document. To avoid name collisions between files, component names may be prefixed with the input file’s base
// name when needed (for example, `users.yaml` ➜ `users.*`). References across files are resolved, and
// later inputs in the array can override earlier ones on conflict.
```

:::

::: info
Expand Down
14 changes: 14 additions & 0 deletions docs/openapi-ts/configuration/output.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ export default {
```
<!-- prettier-ignore-end -->

```js [array]
export default {
input: 'hey-api/backend', // sign up at app.heyapi.dev
output: [
// [!code ++]
'src/client', // [!code ++]
{ path: 'src/client-formatted', format: 'prettier' }, // [!code ++]
{ path: 'src/client-linted', lint: 'eslint', clean: false }, // [!code ++]
], // [!code ++]
};
```

<!-- prettier-ignore-end -->

:::

## Format
Expand Down
192 changes: 191 additions & 1 deletion packages/openapi-ts-tests/main/test/2.0.x.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ describe(`OpenAPI ${version}`, () => {
version,
typeof userConfig.input === 'string'
? userConfig.input
: (userConfig.input.path as string),
: userConfig.input instanceof Array
? (userConfig.input[0] as any).path || userConfig.input[0]
: (userConfig.input as any).path,
);
return {
plugins: ['@hey-api/typescript'],
Expand Down Expand Up @@ -409,4 +411,192 @@ describe(`OpenAPI ${version}`, () => {
}),
);
});

describe('multi config', () => {
it('generates outputs for all configs', async () => {
const configA = createConfig({
input: 'external.yaml',
output: 'multi-external',
});
const configB = createConfig({
input: 'enum-names-values.json',
output: 'multi-enum-names-values',
});

await createClient([configA, configB]);

const outputPathA =
typeof configA.output === 'string'
? configA.output
: configA.output.path;
const outputPathB =
typeof configB.output === 'string'
? configB.output
: configB.output.path;

const filesA = getFilePaths(outputPathA);
const filesB = getFilePaths(outputPathB);

await Promise.all(
filesA.map(async (filePath) => {
const fileContent = fs.readFileSync(filePath, 'utf-8');
await expect(fileContent).toMatchFileSnapshot(
path.join(
__dirname,
'__snapshots__',
version,
filePath.slice(outputDir.length + 1),
),
);
}),
);

await Promise.all(
filesB.map(async (filePath) => {
const fileContent = fs.readFileSync(filePath, 'utf-8');
await expect(fileContent).toMatchFileSnapshot(
path.join(
__dirname,
'__snapshots__',
version,
filePath.slice(outputDir.length + 1),
),
);
}),
);
});
});

describe('multi input', () => {
it('parses multiple inputs (object + string) without errors', async () => {
const specsBase = path.join(getSpecsPath(), version);
await expect(
createClient({
dryRun: true,
input: [
{ path: path.join(specsBase, 'multi-a.json') },
path.join(specsBase, 'multi-b.json'),
],
logs: { level: 'silent' },
output: path.join(outputDir, 'multi-input'),
plugins: ['@hey-api/typescript'],
}),
).resolves.not.toThrow();
});
});

describe('multi output', () => {
it('generates multiple string outputs without errors', async () => {
const results = await createClient({
input: path.join(getSpecsPath(), version, 'external.yaml'),
logs: { level: 'silent' },
output: [
path.join(outputDir, 'multi-output-string-1'),
path.join(outputDir, 'multi-output-string-2'),
],
plugins: ['@hey-api/typescript'],
});

expect(results).toHaveLength(2);

// Verify both output directories were created
expect(fs.existsSync(path.join(outputDir, 'multi-output-string-1'))).toBe(
true,
);
expect(fs.existsSync(path.join(outputDir, 'multi-output-string-2'))).toBe(
true,
);
});

it('generates multiple output objects with different configurations', async () => {
const results = await createClient({
input: path.join(getSpecsPath(), version, 'external.yaml'),
logs: { level: 'silent' },
output: [
{
clean: true,
indexFile: true,
path: path.join(outputDir, 'multi-output-config-1'),
},
{
clean: false,
indexFile: false,
path: path.join(outputDir, 'multi-output-config-2'),
},
],
plugins: ['@hey-api/typescript'],
});

expect(results).toHaveLength(2);

// Verify both output directories were created
expect(fs.existsSync(path.join(outputDir, 'multi-output-config-1'))).toBe(
true,
);
expect(fs.existsSync(path.join(outputDir, 'multi-output-config-2'))).toBe(
true,
);

// Verify index files are created/not created based on configuration
expect(
fs.existsSync(
path.join(outputDir, 'multi-output-config-1', 'index.ts'),
),
).toBe(true);
expect(
fs.existsSync(
path.join(outputDir, 'multi-output-config-2', 'index.ts'),
),
).toBe(false);
});

it('generates mixed string and object outputs', async () => {
const results = await createClient({
input: path.join(getSpecsPath(), version, 'external.yaml'),
logs: { level: 'silent' },
output: [
path.join(outputDir, 'multi-output-mixed-string'),
{
indexFile: false,
path: path.join(outputDir, 'multi-output-mixed-object'),
},
],
plugins: ['@hey-api/typescript'],
});

expect(results).toHaveLength(2);

// Verify both output directories were created
expect(
fs.existsSync(path.join(outputDir, 'multi-output-mixed-string')),
).toBe(true);
expect(
fs.existsSync(path.join(outputDir, 'multi-output-mixed-object')),
).toBe(true);
});

it('preserves global configuration across multiple outputs', async () => {
const results = await createClient({
experimentalParser: true,
input: path.join(getSpecsPath(), version, 'external.yaml'),
logs: { level: 'silent' },
output: [
path.join(outputDir, 'multi-output-global-1'),
path.join(outputDir, 'multi-output-global-2'),
],
plugins: ['@hey-api/typescript', '@hey-api/sdk'],
});

expect(results).toHaveLength(2);

// Both results should have the same global configuration
results.forEach((result) => {
if ('config' in result) {
expect(result.config.experimentalParser).toBe(true);
expect(result.config.plugins['@hey-api/typescript']).toBeDefined();
expect(result.config.plugins['@hey-api/sdk']).toBeDefined();
}
});
});
});
});
Loading
Loading