Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4da54e4
docs: tier 1 CLI improvements design and implementation plan
ducdmdev Feb 26, 2026
bebbac0
feat(ir): add circular reference detection test
ducdmdev Feb 26, 2026
1870ca9
feat(config): add split, baseURL, apiFetchImportPath to ConfigInput
ducdmdev Feb 26, 2026
0b4b4f9
fix(loader): improve error messages for file not found and parse fail…
ducdmdev Feb 26, 2026
82b083e
feat(ir): support allOf composition via property merging
ducdmdev Feb 26, 2026
12ed549
feat(generators): wire baseURL into apiFetch and hooks generation
ducdmdev Feb 26, 2026
f836682
fix(ir): warn when spec has no paths
ducdmdev Feb 26, 2026
3072aa8
feat(writer): pass baseURL and apiFetchImportPath through to generators
ducdmdev Feb 26, 2026
10d4018
test(e2e): add integration test for allOf composition
ducdmdev Feb 26, 2026
ff8e948
fix(loader): add hint suffix to parse error message per design doc
ducdmdev Feb 26, 2026
6cb0a9a
feat(cli): add config file loading with --config flag and auto-search
ducdmdev Feb 26, 2026
b135be1
fix(test): use precise assertion for empty paths warning message
ducdmdev Feb 26, 2026
e36be85
feat(cli): add --dry-run flag with interactive confirmation prompt
ducdmdev Feb 26, 2026
22225e0
feat(cli): add interactive config init wizard with @inquirer/prompts
ducdmdev Feb 26, 2026
dc0267b
chore: bump version to 0.3.0
ducdmdev Feb 26, 2026
1fa8f2a
docs: sync documentation with v0.3.0 tier 1 CLI improvements
ducdmdev Feb 26, 2026
eb4ada6
fix(mocks): generate {} for object-typed props with faker-matching names
ducdmdev Feb 26, 2026
7fb86bb
docs: update test count to 88
ducdmdev Feb 26, 2026
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ apigen is a standalone npm CLI that reads OpenAPI 3.0+ and Swagger 2.0 specs and
## Commands

```bash
bun test # run tests (56 tests across 11 files)
bun test # run tests (88 tests across 13 files)
bun run typecheck # tsc --noEmit
bun run build # compile to dist/
```
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ function PetList() {
- **Mock data + test mode** — static mocks and a React context provider to toggle them on
- **Swagger 2.0 support** — auto-converted to OpenAPI 3.x
- **Flat or split output** — single directory or split by API tag with `--split`
- **Config file support** — `apigen.config.ts` with auto-discovery or `--config` flag
- **Interactive wizard** — run without flags to be guided through setup
- **allOf composition** — schema merging for specs using `allOf`
- **Dry-run mode** — preview generated files with `--dry-run` before writing

## Install

Expand All @@ -49,7 +53,17 @@ npm install -D apigen-tanstack
## Usage

```bash
# From a local file
npx apigen-tanstack generate --input ./openapi.yaml --output ./src/api/generated

# From a config file
npx apigen-tanstack generate --config apigen.config.ts

# Interactive mode (omit flags to be guided through setup)
npx apigen-tanstack generate

# Preview without writing
npx apigen-tanstack generate -i ./openapi.yaml --dry-run
```

## Generated Files
Expand Down
42 changes: 28 additions & 14 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ Creates a fully resolved configuration object. This is the recommended way to de
`defineConfig` delegates to `resolveConfig` internally -- it exists as a named entrypoint so config files read naturally.

```ts
import { defineConfig } from 'apigen'
import { defineConfig } from 'apigen-tanstack'

export default defineConfig({
input: './openapi.yaml',
output: './src/api/generated',
mock: true,
split: false,
baseURL: 'https://api.example.com',
})
```

Expand All @@ -35,11 +37,12 @@ export default defineConfig({
Resolves a partial `ConfigInput` into a complete `Config` by applying default values. Useful when you build config objects programmatically rather than through a config file.

```ts
import { resolveConfig } from 'apigen'
import { resolveConfig } from 'apigen-tanstack'

const config = resolveConfig({ input: './spec.yaml' })
// config.output => './src/api/generated'
// config.mock => true
// config.split => false
```

**Parameters**
Expand All @@ -56,23 +59,29 @@ const config = resolveConfig({ input: './spec.yaml' })

### `Config`

The fully resolved configuration object. Every field is required.
The fully resolved configuration object. Required fields are always present; optional fields may be `undefined`.

| Field | Type | Description |
|----------|-----------|--------------------------------------------------------------------|
| `input` | `string` | Path to the OpenAPI or Swagger spec file (JSON or YAML). |
| `output` | `string` | Directory where generated files are written. |
| `mock` | `boolean` | Whether to generate mock data alongside types and hooks. |
| Field | Type | Description |
|----------------------|-----------|--------------------------------------------------------------------|
| `input` | `string` | Path to the OpenAPI or Swagger spec file (JSON or YAML). |
| `output` | `string` | Directory where generated files are written. |
| `mock` | `boolean` | Whether to generate mock data alongside types and hooks. |
| `split` | `boolean` | Whether to split output into per-tag feature folders. |
| `baseURL` | `string?` | Base URL prefix for all generated fetch paths. |
| `apiFetchImportPath` | `string?` | Custom import path for the apiFetch function. |

### `ConfigInput`

The input type accepted by `defineConfig` and `resolveConfig`. Only `input` is required; all other fields are optional and fall back to defaults.

| Field | Type | Required | Default | Description |
|----------|-----------|----------|--------------------------|--------------------------------------------------|
| `input` | `string` | Yes | -- | Path to the OpenAPI or Swagger spec file. |
| `output` | `string` | No | `'./src/api/generated'` | Output directory for generated files. |
| `mock` | `boolean` | No | `true` | Generate mock data files. |
| Field | Type | Required | Default | Description |
|----------------------|-----------|----------|--------------------------|--------------------------------------------------|
| `input` | `string` | Yes | -- | Path to the OpenAPI or Swagger spec file. |
| `output` | `string` | No | `'./src/api/generated'` | Output directory for generated files. |
| `mock` | `boolean` | No | `true` | Generate mock data files. |
| `split` | `boolean` | No | `false` | Split output into per-tag feature folders. |
| `baseURL` | `string` | No | *(none)* | Base URL prefix for all fetch paths. |
| `apiFetchImportPath` | `string` | No | *(none)* | Custom import path for apiFetch function. |

---

Expand All @@ -96,8 +105,13 @@ apigen generate -i ./openapi.yaml
| `-o, --output <path>` | No | `./src/api/generated` | Output directory for generated files. |
| `--no-mock` | No | (mock enabled) | Skip mock data generation. |
| `--split` | No | (disabled) | Split output into per-tag feature folders. |
| `-c, --config <path>` | No | *(auto-searches)* | Path to config file. Auto-searches for `apigen.config.ts`/`.js` when omitted. |
| `--base-url <url>` | No | *(none)* | Base URL prefix for all generated fetch paths. |
| `--dry-run` | No | (disabled) | Preview generated files with sizes without writing. In TTY, prompts to proceed. |

> **Interactive mode:** When `-i` is omitted, apigen prompts you to choose between providing a local file path, a direct URL, or auto-discovering a spec from a base URL (tries well-known paths like `/v3/api-docs`, `/swagger.json`, `/openapi.json`).
> **Interactive mode:** When `-i` is omitted and no config file is found, apigen runs an interactive wizard: choose spec source (file, URL, or auto-discover), configure output/mock/split/baseURL options, and optionally save as `apigen.config.ts`.

> **Config file priority:** CLI flags override config file values. Config file values override defaults.

**Examples**

Expand Down
47 changes: 37 additions & 10 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ function generateSomething(ir: IR): string {
| Generator | File | Signature | Description |
|---|---|---|---|
| `generateTypes` | `generators/types.ts` | `(ir: IR) => string` | Schema interfaces + param interfaces |
| `generateApiFetch` | `generators/api-fetch.ts` | `(options?) => string` | Shared apiFetch helper (used in split mode) |
| `generateHooks` | `generators/hooks.ts` | `(ir: IR, options?) => string` | useQuery/useMutation hooks, apiFetch helper, imports |
| `generateMocks` | `generators/mocks.ts` | `(ir: IR) => string` | Schema mocks + response mocks |
| `generateProvider` | `generators/provider.ts` | `() => string` | Static ApiTestModeProvider context (no IR needed) |
Expand Down Expand Up @@ -263,9 +264,9 @@ Returns a static string literal with `export * from` statements for each of the

## Stage 4: File Writer (`src/writer.ts`)

**Entry point:** `writeGeneratedFiles(ir: IR, outputDir: string, options?: { mock?: boolean; split?: boolean }): void`
**Entry point:** `writeGeneratedFiles(ir: IR, outputDir: string, options?: { mock?: boolean; split?: boolean; baseURL?: string; apiFetchImportPath?: string; dryRun?: boolean }): FileInfo[] | void`

When `mock` is `false`, mocks and provider files are skipped. When `split` is `true`, output is organized into per-tag feature folders.
When `mock` is `false`, mocks and provider files are skipped. When `split` is `true`, output is organized into per-tag feature folders. When `dryRun` is `true`, returns an array of `FileInfo` objects (path + size) without writing. The `baseURL` and `apiFetchImportPath` options are passed through to the hooks and api-fetch generators.

The writer is the orchestrator. It:

Expand Down Expand Up @@ -341,7 +342,7 @@ The key constraint is that generators must be **pure functions**: `IR` in, `stri
The CLI ties everything together:

```
apigen generate -i <spec-path> [-o <output-dir>]
apigen generate -i <spec-path> [-o <output-dir>] [--config <path>] [--base-url <url>] [--dry-run] [--split] [--no-mock]
```

| Flag | Default | Description |
Expand All @@ -350,17 +351,37 @@ apigen generate -i <spec-path> [-o <output-dir>]
| `-o, --output <path>` | `./src/api/generated` | Output directory for generated files |
| `--no-mock` | mocks enabled | Skip mock data generation |
| `--split` | disabled | Split output into per-tag feature folders |
| `-c, --config <path>` | *(auto-searches)* | Path to config file |
| `--base-url <url>` | *(none)* | Base URL prefix for fetch paths |
| `--dry-run` | disabled | Preview files without writing |

Internally, it runs the pipeline in sequence:
### Config file loading

The CLI loads configuration in this priority order:

1. **Explicit `--config` flag** — loads the specified file
2. **Auto-search** — looks for `apigen.config.ts` or `apigen.config.js` in the current directory
3. **CLI flags** — override any config file values
4. **Interactive wizard** — runs when no config file and no `-i` flag are provided

Config files are loaded via dynamic `import()` and must export a `ConfigInput` object (or use `defineConfig` for type safety).

### Pipeline

Internally, the CLI runs the pipeline in sequence:

```ts
const inputValue = options.input ?? (await promptForInput()) // Interactive if -i omitted
const fileConfig = await loadConfigFile(configPath) // Load config file (if found)
const config = resolveConfig({ ...fileConfig, ...cliFlags }) // Merge with CLI overrides
const inputValue = config.input || (await promptForInput()) // Interactive if no input
const spec = await loadSpec(inputPath) // Stage 1: load + normalize
const ir = extractIR(spec) // Stage 2: extract IR
writeGeneratedFiles(ir, outputPath, { mock, split }) // Stage 3+4: generate + write
writeGeneratedFiles(ir, outputPath, { mock, split, baseURL, apiFetchImportPath, dryRun }) // Stage 3+4
```

When `-i` is omitted, `promptForInput()` (from `@inquirer/prompts`) offers three choices: local file path, direct URL, or auto-discover from a base URL using `discoverSpec()` from `src/discover.ts`.
When no config file is found and `-i` is omitted, the full interactive wizard runs: `promptForInput()` for spec source, then `promptForConfig()` for output/mock/split/baseURL options, with an option to save as `apigen.config.ts`.

The `--dry-run` flag invokes `collectFileInfo()` to calculate output file sizes without writing, then displays a preview. In TTY mode, the user is prompted to confirm before writing.

The CLI logs the number of operations and schemas found, and the output directory path.

Expand Down Expand Up @@ -390,15 +411,21 @@ For programmatic use, apigen exports `defineConfig` and `resolveConfig`:

```ts
interface ConfigInput {
input: string // required: path or URL to spec
output?: string // default: './src/api/generated'
mock?: boolean // default: true
input: string // required: path or URL to spec
output?: string // default: './src/api/generated'
mock?: boolean // default: true
split?: boolean // default: false
baseURL?: string // prefix for all fetch paths
apiFetchImportPath?: string // custom import path for apiFetch
}

interface Config {
input: string
output: string
mock: boolean
split: boolean
baseURL?: string
apiFetchImportPath?: string
}
```

Expand Down
96 changes: 92 additions & 4 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,40 @@ Use `defineConfig` in a TypeScript config file to get type-checked configuration

```ts
// apigen.config.ts
import { defineConfig } from 'apigen'
import { defineConfig } from 'apigen-tanstack'

export default defineConfig({
input: './specs/petstore.yaml',
output: './src/api/generated',
mock: true,
split: false,
baseURL: 'https://api.example.com',
})
```

`defineConfig` accepts a `ConfigInput` object and returns a fully resolved `Config` with defaults applied. It is a pure helper for type safety -- it does not read files or trigger generation.

The CLI auto-searches for `apigen.config.ts` or `apigen.config.js` in the current directory when no `--config` flag is provided.

### Type signature

```ts
interface ConfigInput {
input: string // required
output?: string // optional, defaults to './src/api/generated'
mock?: boolean // optional, defaults to true
input: string // required
output?: string // optional, defaults to './src/api/generated'
mock?: boolean // optional, defaults to true
split?: boolean // optional, defaults to false
baseURL?: string // optional, prefix for all fetch paths
apiFetchImportPath?: string // optional, custom import path for apiFetch
}

interface Config {
input: string
output: string
mock: boolean
split: boolean
baseURL?: string
apiFetchImportPath?: string
}

function defineConfig(config: ConfigInput): Config
Expand Down Expand Up @@ -89,6 +99,54 @@ defineConfig({

> **Note:** When `mock` is `false`, the `mocks.ts` and `test-mode-provider.tsx` files are not generated, and hooks do not include test mode logic.

### `split`

Split generated output into per-tag feature folders. Each tag gets its own directory with `types.ts`, `hooks.ts`, `mocks.ts`, and `index.ts`.

| | |
|---|---|
| **Type** | `boolean` |
| **Default** | `false` |

```ts
defineConfig({
input: './openapi.yaml',
split: true,
})
```

### `baseURL`

Base URL prefix for all generated fetch paths. When set, the generated `apiFetch` helper prepends this URL to every request path.

| | |
|---|---|
| **Type** | `string` |
| **Default** | *(none — paths are relative)* |

```ts
defineConfig({
input: './openapi.yaml',
baseURL: 'https://api.example.com',
})
```

### `apiFetchImportPath`

Custom import path for the `apiFetch` function. Use this when you have your own fetch wrapper and want generated hooks to import from it instead of using the built-in one.

| | |
|---|---|
| **Type** | `string` |
| **Default** | *(none — uses built-in apiFetch)* |

```ts
defineConfig({
input: './openapi.yaml',
apiFetchImportPath: '../lib/api-client',
})
```

## CLI Flags

```bash
Expand Down Expand Up @@ -128,12 +186,38 @@ Split generated output into per-tag feature folders. Each tag gets its own direc
npx apigen generate -i ./openapi.yaml --split
```

### `--config` / `-c`

Path to a config file. When omitted, the CLI searches for `apigen.config.ts` or `apigen.config.js` in the current directory.

```bash
npx apigen generate --config ./config/apigen.config.ts
npx apigen generate -c apigen.config.ts
```

### `--base-url`

Base URL prefix for all generated fetch paths. Overrides the `baseURL` config option.

```bash
npx apigen generate -i ./openapi.yaml --base-url https://api.example.com
```

### `--dry-run`

Preview the files that would be generated (with sizes) without writing anything. In a TTY, you are prompted to proceed after the preview. In non-TTY (CI), it prints and exits.

```bash
npx apigen generate -i ./openapi.yaml --dry-run
```

### Full example

```bash
npx apigen generate \
--input ./specs/petstore.yaml \
--output ./src/api \
--base-url https://api.example.com \
--no-mock \
--split
```
Expand Down Expand Up @@ -173,3 +257,7 @@ The output directory contains these files (mocks and provider are omitted when `
| `output` | `--output` / `-o` | `./src/api/generated` |
| `mock` | `--no-mock` to disable | `true` |
| `split` | `--split` | `false` |
| `baseURL` | `--base-url` | *(none)* |
| `apiFetchImportPath` | *(config only)* | *(none)* |
| — | `--config` / `-c` | *(auto-searches for apigen.config.ts)* |
| — | `--dry-run` | *(disabled)* |
5 changes: 5 additions & 0 deletions docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,14 @@ tests/
hooks.test.ts
mocks.test.ts
provider.test.ts
api-fetch.test.ts
index-file.test.ts
fixtures/
petstore-oas3.yaml
petstore-swagger2.yaml
inline-schemas.yaml
tagged-api.yaml
allof-composition.yaml
```

## Scripts
Expand Down
Loading