@shipworthy/openapi-cli 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +170 -106
  2. package/dist/bin.js +2 -1
  3. package/dist/cli.d.ts +1 -1
  4. package/dist/commands/generate/command.d.ts +19 -2
  5. package/dist/commands/generate/command.js +74 -6
  6. package/dist/commands/generate/{plugins/typescript/flags.d.ts → flags.d.ts} +5 -4
  7. package/dist/commands/generate/{plugins/typescript/flags.js → flags.js} +5 -12
  8. package/dist/commands/generate/output.d.ts +15 -0
  9. package/dist/commands/generate/output.js +35 -0
  10. package/dist/commands/generate/plan.d.ts +53 -0
  11. package/dist/commands/generate/plan.js +93 -0
  12. package/dist/commands/generate/render.d.ts +25 -0
  13. package/dist/commands/generate/render.js +63 -0
  14. package/dist/commands/generate/source.d.ts +5 -0
  15. package/dist/commands/generate/source.js +18 -0
  16. package/dist/commands/generate/stamp.d.ts +14 -10
  17. package/dist/commands/generate/stamp.js +16 -28
  18. package/dist/commands/input.js +2 -2
  19. package/dist/commands/overlay/command.js +4 -3
  20. package/package.json +18 -17
  21. package/dist/commands/generate/ensure.d.ts +0 -9
  22. package/dist/commands/generate/ensure.js +0 -31
  23. package/dist/commands/generate/files.d.ts +0 -4
  24. package/dist/commands/generate/files.js +0 -16
  25. package/dist/commands/generate/force.d.ts +0 -2
  26. package/dist/commands/generate/force.js +0 -2
  27. package/dist/commands/generate/plugins/standard-schema/plugin.d.ts +0 -7
  28. package/dist/commands/generate/plugins/standard-schema/plugin.js +0 -44
  29. package/dist/commands/generate/plugins/tanstack-query/plugin.d.ts +0 -9
  30. package/dist/commands/generate/plugins/tanstack-query/plugin.js +0 -42
  31. package/dist/commands/generate/plugins/typescript/plugin.d.ts +0 -32
  32. package/dist/commands/generate/plugins/typescript/plugin.js +0 -84
  33. package/dist/commands/generate/plugins/zod/plugin.d.ts +0 -8
  34. package/dist/commands/generate/plugins/zod/plugin.js +0 -48
  35. package/dist/commands/generate/publish.d.ts +0 -11
  36. package/dist/commands/generate/publish.js +0 -23
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @shipworthy/openapi-cli
2
2
 
3
- `@shipworthy/openapi-cli` installs the `openapi` command. It bundles, overlays, and lints OpenAPI 3.x documents and generates code from them: TypeScript types, Zod validators, Standard Schema validators, and typed TanStack Query clients. Use it in a build step or `generate` script that keeps generated API code in sync with a local or remote OpenAPI document. Each generator is a separate optional package (`@shipworthy/openapi-typescript`, `@shipworthy/openapi-zod`, `@shipworthy/openapi-standard-schema`, `@shipworthy/openapi-tanstack-query`). The CLI loads a generator only when you run its command, so install just the ones you use.
3
+ `@shipworthy/openapi-cli` installs the `openapi` command. It bundles, overlays, and lints OpenAPI 3.x documents and generates code from them: TypeScript types, Zod validators, Standard Schema validators, and typed TanStack Query clients. Use it in a build step or `generate` script that keeps generated API code in sync with a local or remote OpenAPI document. Each generator is a separate optional package (`@shipworthy/openapi-typescript`, `@shipworthy/openapi-zod`, `@shipworthy/openapi-standard-schema`, `@shipworthy/openapi-tanstack-query`). One `openapi generate` run can select several generator plugins and writes one file per plugin into an output directory. The CLI loads a generator only when a run selects its plugin, so install just the ones you use.
4
4
 
5
5
  ## Requirements
6
6
 
@@ -10,31 +10,31 @@
10
10
 
11
11
  ## Installation
12
12
 
13
- Install the CLI and the generator packages you need as dev dependencies. The CLI brings its own dependencies (`effect` `^4.0.0`, `@effect/platform-node` `^4.0.0`, `@shipworthy/openapi-generator`, `@shipworthy/openapi-parser`, `@shipworthy/overlay`, and `@shipworthy/openapi-validator`):
13
+ Install the CLI and the generator packages you need as dev dependencies. The CLI brings its own dependencies (`effect`, `@effect/platform-node`, and `@effect/platform-node-shared` pinned to `4.1.4`, plus `@shipworthy/openapi-generator`, `@shipworthy/openapi-parser`, `@shipworthy/openapi-overlay`, and `@shipworthy/openapi-validator`):
14
14
 
15
15
  ```sh
16
- bun add -d @shipworthy/openapi-cli @shipworthy/openapi-typescript effect@^4
16
+ bun add -d @shipworthy/openapi-cli @shipworthy/openapi-typescript
17
17
  ```
18
18
 
19
19
  ### Optional peer dependencies (generators)
20
20
 
21
- Install the generator package for each `generate` subcommand you use, at the same version as the CLI:
21
+ Install the generator package for each `generate` plugin you use, at the same version as the CLI:
22
22
 
23
- | Command | Requires | Notes |
24
- | ---------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------- |
25
- | `openapi generate typescript` | `@shipworthy/openapi-typescript` | That package peers on `effect@^4.0.0`. |
26
- | `openapi generate zod` | `@shipworthy/openapi-zod` and `@shipworthy/openapi-typescript` | Also writes `types.ts`. |
27
- | `openapi generate tanstack-query` | `@shipworthy/openapi-tanstack-query` and `@shipworthy/openapi-typescript` | Also writes `types.ts`. |
28
- | `openapi generate standard-schema` | `@shipworthy/openapi-standard-schema` | |
29
- | `openapi lint` | nothing extra | Bundled through `@shipworthy/openapi-validator` (Spectral). |
23
+ | Plugin / command | Requires | Notes |
24
+ | ----------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------- |
25
+ | `typescript` | `@shipworthy/openapi-typescript` | Writes `types.ts`. |
26
+ | `zod` | `@shipworthy/openapi-zod` and `@shipworthy/openapi-typescript` | Writes `zod-schema.ts`; also needs `types.ts`. |
27
+ | `tanstack-query` | `@shipworthy/openapi-tanstack-query` and `@shipworthy/openapi-typescript` | Writes `client.ts`; also needs `types.ts`. |
28
+ | `standard-schema` | `@shipworthy/openapi-standard-schema` | Writes `standard-schema.ts`. |
29
+ | `openapi lint` | nothing extra | Bundled through `@shipworthy/openapi-validator` (Spectral). |
30
30
 
31
31
  The generated files import packages at runtime, so your application needs those packages too:
32
32
 
33
33
  | Generated file | Imports |
34
34
  | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
35
35
  | `types.ts` (TypeScript) | nothing (types only) |
36
- | Zod output | `zod` (v4; the generator is built against `zod@4.6.5`) and `./types` |
37
- | Standard Schema output | `effect` (`^4.0.0`) |
36
+ | `zod-schema.ts` (Zod) | `zod` (v4; the generator is built against `zod@4.6.7`) and `./types` |
37
+ | `standard-schema.ts` | `effect` (`4.1.4`) |
38
38
  | `client.ts` (TanStack Query) | `@shipworthy/openapi-tanstack-query` (a runtime dependency), which peers on `@tanstack/react-query@^5.104.1` and `react@^18.0.0 \|\| ^19.0.0` |
39
39
 
40
40
  ## Quick start
@@ -42,11 +42,13 @@ The generated files import packages at runtime, so your application needs those
42
42
  Run the `openapi` commands below in a package script, or use `bunx --bun @shipworthy/openapi-cli` in place of `openapi`.
43
43
 
44
44
  ```sh
45
- bun add -d @shipworthy/openapi-cli @shipworthy/openapi-typescript effect@^4
45
+ bun add -d @shipworthy/openapi-cli @shipworthy/openapi-typescript
46
46
  bunx --bun @shipworthy/openapi-cli lint ./openapi.yaml
47
- bunx --bun @shipworthy/openapi-cli generate typescript ./openapi.yaml -o ./src/generated/types.ts
47
+ bunx --bun @shipworthy/openapi-cli generate typescript -i ./openapi.yaml -o ./src/generated
48
48
  ```
49
49
 
50
+ The `generate` command writes `src/generated/types.ts`.
51
+
50
52
  Given this `openapi.yaml`:
51
53
 
52
54
  ```yaml
@@ -106,12 +108,12 @@ A typical `package.json` script that generates a full SDK (from the repository's
106
108
  ```json
107
109
  {
108
110
  "scripts": {
109
- "generate": "openapi generate tanstack-query openapi.json -o src/generated --name hotels && openapi generate zod openapi.json -o src/generated/hotels/zod-schema.ts && openapi generate standard-schema openapi.json -o src/generated/hotels/standard-schema.ts"
111
+ "generate": "openapi generate tanstack-query zod standard-schema -i openapi.json -o src/generated/hotels"
110
112
  }
111
113
  }
112
114
  ```
113
115
 
114
- That script produces `src/generated/hotels/{types.ts,client.ts,zod-schema.ts,standard-schema.ts}`.
116
+ That script produces `src/generated/hotels/{types.ts,client.ts,zod-schema.ts,standard-schema.ts}`. The client name `hotels` comes from the output directory.
115
117
 
116
118
  ## Commands
117
119
 
@@ -121,13 +123,13 @@ openapi <subcommand> [flags]
121
123
  SUBCOMMANDS
122
124
  bundle Bundle an OpenAPI document
123
125
  overlay Apply an OpenAPI Overlay
124
- generate Run a generator plugin
126
+ generate Generate types, validators, and clients
125
127
  lint Lint an OpenAPI schema
126
128
  ```
127
129
 
128
130
  ### Input argument (all commands)
129
131
 
130
- Every command takes a positional `<input>`:
132
+ `bundle`, `overlay`, and `lint` take a positional `<input>`. `generate` takes the input with `-i`/`--input` and reads the document from stdin when the flag is omitted. In both forms the input is:
131
133
 
132
134
  - A local path, relative to the current directory, e.g. `./openapi.yaml`.
133
135
  - An `http:`, `https:`, or `file:` URL. Remote documents are fetched over HTTP, and the response must have a 2xx status.
@@ -181,7 +183,9 @@ openapi overlay openapi.yaml --overlay selected.overlay.yaml --remove-unused-com
181
183
 
182
184
  Actions run sequentially against the bundled document. Supports RFC 9535 JSONPath, recursive
183
185
  object updates, array concatenation/appending, primitive replacement, copying from a single
184
- source node, and removal. A target matching no nodes has no effect. `extends` is metadata;
186
+ source node, and removal. Invalid JSONPath target expressions and targets matching no nodes
187
+ are skipped; the CLI logs each as a warning to stderr with its action number and target, then
188
+ writes the result of the remaining actions. `extends` is metadata;
185
189
  `<input>` determines the document. The overlaid result is written directly without another
186
190
  bundling or validation pass.
187
191
 
@@ -196,7 +200,7 @@ writing output. See the overlay package's operation filtering example below.
196
200
  An existing output file is overwritten only after bundling, overlay application, and optional pruning succeed.
197
201
  Loading, invalid overlay, incompatible merge, JSONPath, pruning, and write errors exit with code `1`.
198
202
  Action errors include the one-based action number and target. See the
199
- [`@shipworthy/overlay` documentation](../openapi-overlay/README.md) for merge rules and errors.
203
+ [`@shipworthy/openapi-overlay` documentation](../openapi-overlay/README.md) for merge rules and errors.
200
204
 
201
205
  ### `openapi lint [--strict] <input>`
202
206
 
@@ -224,22 +228,48 @@ ERROR
224
228
  Every operation must have unique "operationId". at #/paths//b/get/operationId
225
229
  ```
226
230
 
227
- ### `openapi generate typescript [<input>]`
231
+ ### `openapi generate <plugins...>`
232
+
233
+ Runs one or more generator plugins against one OpenAPI document and writes one file per plugin into an output directory.
228
234
 
229
- Converts an OpenAPI document to TypeScript types (`paths`, `webhooks`, `components`, `operations`, …) and formats the result. Requires `@shipworthy/openapi-typescript`.
235
+ ```text
236
+ openapi generate <plugins...> [-i <input>] [-o <dir>] [--name <name>] [--camel-case] [--wire-types] [--check] [type flags]
237
+ ```
230
238
 
231
239
  ```sh
232
- openapi generate typescript ./openapi.yaml -o ./schema.ts
233
- openapi generate typescript https://example.com/openapi.json > schema.ts
234
- cat openapi.yaml | openapi generate typescript -o ./schema.ts
235
- openapi generate typescript ./openapi.yaml -o ./schema.ts --check # CI drift check
240
+ openapi generate zod tanstack-query -i ./openapi.json -o src/generated/reservations --camel-case --wire-types
241
+ # writes src/generated/reservations/{client.ts,types.ts,wire-types.ts,zod-schema.ts}
242
+
243
+ openapi generate typescript -i ./openapi.yaml -o ./src/generated # writes src/generated/types.ts
244
+ openapi generate typescript -i https://example.com/openapi.json > schema.ts # one file, printed to stdout
245
+ cat openapi.yaml | openapi generate standard-schema > schemas.ts # reads stdin
246
+ openapi generate typescript zod -i ./openapi.yaml -o ./src/generated --check # CI drift check
236
247
  ```
237
248
 
238
- | Argument / flag | Type | Default | Description |
239
- | ---------------- | ----------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
240
- | `<input>` | path or URL | stdin | Optional. Reads the document from stdin when omitted. |
241
- | `--output`, `-o` | file path | stdout | Output file. Parent directories are created. Writes to stdout when omitted. |
242
- | `--check` | boolean | `false` | Writes nothing. Compares the generated output with the existing `--output` file and exits `1` with `Generated types are not up-to-date!` when they differ. Use it together with `-o`. |
249
+ #### Plugins
250
+
251
+ Pass at least one plugin. Each plugin writes one file with a fixed name:
252
+
253
+ | Plugin | Package (optional peer) | File | Also needs `types.ts` |
254
+ | ----------------- | ------------------------------------- | -------------------- | --------------------- |
255
+ | `typescript` | `@shipworthy/openapi-typescript` | `types.ts` | (it is `types.ts`) |
256
+ | `zod` | `@shipworthy/openapi-zod` | `zod-schema.ts` | yes |
257
+ | `tanstack-query` | `@shipworthy/openapi-tanstack-query` | `client.ts` | yes |
258
+ | `standard-schema` | `@shipworthy/openapi-standard-schema` | `standard-schema.ts` | no |
259
+
260
+ `zod` and `tanstack-query` import their types from `./types`, so they also require `@shipworthy/openapi-typescript` to be installed. The order of plugins does not matter, and a plugin listed twice runs once. Any order produces identical files.
261
+
262
+ #### Options
263
+
264
+ | Flag | Type | Default | Description |
265
+ | ---------------- | ------------------------------------ | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
266
+ | `--input`, `-i` | path or `http:`/`https:`/`file:` URL | stdin | OpenAPI document (JSON or YAML). Reads the document from stdin when omitted. An input containing `*` fails with `Globbing is unsupported.` |
267
+ | `--output`, `-o` | directory | stdout | Directory that receives the files. It is created recursively. When omitted, the run must produce exactly one file, which is written to stdout. |
268
+ | `--name` | string | basename of the output directory | Used only by `tanstack-query`: the PascalCase prefix of generated exports and the query-key prefix. Must match `^[a-zA-Z][a-zA-Z0-9]*(?:[-_][a-zA-Z0-9]+)*$`. Ignored when `tanstack-query` is not selected. See [Client name](#client-name). |
269
+ | `--camel-case` | boolean | `false` | Spell snake_case field and parameter names in camelCase in every generated file. See [camelCase and wire types](#camelcase-and-wire-types). |
270
+ | `--wire-types` | boolean | `false` | Also write `wire-types.ts`: the same types as `types.ts` with the API's field names. Requires `--camel-case` and a plugin that writes `types.ts`. |
271
+ | `--check` | boolean | `false` | Writes nothing. Compares every generated file with the existing file in `--output` and exits `1` when any differs or is missing. Requires `--output`. |
272
+ | type flags | boolean | `false` (`--array-length`: `true`) | See the next table. They apply to `types.ts` and `wire-types.ts` and are ignored when neither is generated. |
243
273
 
244
274
  Type-generation flags. All are boolean and default to `false` except `--array-length`, which
245
275
  defaults to `true`:
@@ -252,39 +282,74 @@ defaults to `true`:
252
282
  | `--dedupe-enums` | Dedupe enum types when `--enum` is set. |
253
283
  | `--export-type`, `-t` | Export top-level type aliases instead of interfaces. |
254
284
  | `--array-length` | Generate tuples using array `minItems` / `maxItems`. Enabled by default; disable with `--array-length=false` or `--no-array-length`. |
285
+ | `--camel-case` | Spell snake_case schema fields and parameter names in camelCase. Applies to every generated file, not only `types.ts`. |
255
286
  | `--root-types` | Export component and operation aliases at the root level. Schema aliases have no prefix; operation aliases uppercase the first character of the operation ID. |
256
287
  | `--read-write-markers` | Generate `$Read`/`$Write` markers for `readOnly`/`writeOnly` properties. |
257
288
 
258
- The output always includes an `ApiPaths` enum with route values. Paths retain their literal
259
- OpenAPI templates, such as `/pets/{id}`.
289
+ #### Which files are written
260
290
 
261
- With `-o`, the command logs `<input> → <output> [<elapsed>]` when it finishes.
291
+ A run writes:
262
292
 
263
- Use an overlay before generation to require properties, allow additional keys, remove
264
- deprecated members, or declare path parameters. See the
265
- [overlay examples](../openapi-overlay/README.md#preparing-a-document-for-generation).
293
+ - `types.ts` when `typescript`, `zod`, or `tanstack-query` is selected. It is generated once and shared by `zod-schema.ts` and `client.ts`.
294
+ - `zod-schema.ts`, `client.ts`, and `standard-schema.ts` for each selected plugin.
295
+ - `wire-types.ts` when `--wire-types` is set.
266
296
 
267
- ```sh
268
- openapi overlay openapi.yaml --overlay types.overlay.yaml -o types.openapi.json
269
- openapi generate typescript types.openapi.json -o schema.ts
297
+ File names are fixed and `-o` is always a directory. An `--output` that ends in `.ts`, `.mts`, `.cts`, `.js`, `.mjs`, `.cjs`, or `.json` is rejected. Every run regenerates and overwrites every file it writes.
298
+
299
+ #### Client name
300
+
301
+ `tanstack-query` needs a client name. Without `--name`, it is the basename of the output directory: `-o src/generated/hotels` gives `hotels`. The name in PascalCase is the prefix of the generated exports, and a `-` or `_` marks a word boundary: `-o src/generated/my-pets` gives `createMyPetsApiClient`. The raw name is the default query-key prefix.
302
+
303
+ When the basename does not match the name pattern, the run fails with `Cannot derive a client name from the output directory "<basename>". Pass --name.`
304
+
305
+ #### camelCase and wire types
306
+
307
+ `--camel-case` applies to every generated file:
308
+
309
+ - `types.ts` spells snake_case schema fields and parameter names in camelCase.
310
+ - `client.ts` converts declared parameter and JSON body fields between camelCase and the API's names at runtime.
311
+ - `zod-schema.ts` validators accept and return the camelCase shapes declared in `types.ts`.
312
+ - `standard-schema.ts` exports camelCase types and validators.
313
+
314
+ Enum and const values keep the API's spelling. When two property names at one object level become the same camelCase name, generation fails with a message such as `#/components/schemas/User: user_id and userId both become userId`, and nothing is written.
315
+
316
+ `--wire-types` also writes `wire-types.ts`: the same types as `types.ts`, with the API's field names. Import its `operations` or `components` types when a custom `fetch` or mock `Response` handles the API's snake_case payload before client conversion. It requires `--camel-case` and at least one of `typescript`, `zod`, or `tanstack-query`.
317
+
318
+ #### stdin and stdout
319
+
320
+ - Without `-i`, the document is read from stdin. Invalid invocations fail before stdin is read.
321
+ - Without `-o`, the run must produce exactly one file. Only that file is written to stdout, with no log output, and the `typescript` plugin's warnings are suppressed. A run that would produce more than one file (for example `zod`, which also needs `types.ts`) fails with `--output is required when generating more than one file.`
322
+
323
+ #### CI drift check (`--check`)
324
+
325
+ `--check` writes nothing. It generates every file the run would write and compares each one with the existing file in `--output`. When any file differs or is missing, it exits `1` with `Generated files are not up-to-date: <file names>`, listing the file names sorted and separated by `, `. A file that cannot be read counts as out of date. When every file matches, `--check` prints nothing and exits `0`. `--check` requires `--output`.
326
+
327
+ #### How a run executes
328
+
329
+ The document is read and parsed once, however many plugins run. All files are then rendered concurrently from that document. Nothing is written unless every file was generated successfully; if any generator fails, the output directory is left untouched. Invalid invocations fail before the input is read. With `-o`, after all files are written, the command logs one line per file, sorted by file name:
330
+
331
+ ```text
332
+ <input URL or stdin> → <output>/<file> [<elapsed>]
270
333
  ```
271
334
 
272
- ### `openapi generate zod <input> -o <file>`
335
+ The input is shown as a URL (`file:///…` for local paths) or as `stdin`, and the output directory as you typed it, without trailing slashes. Every line of one run shows the same elapsed time.
336
+
337
+ #### `typescript` output (`types.ts`)
273
338
 
274
- Generates Zod validators. Requires `@shipworthy/openapi-zod` and `@shipworthy/openapi-typescript`.
339
+ Converts the OpenAPI document to TypeScript types (`paths`, `webhooks`, `components`, `operations`, …) and formats the result. The output always includes an `ApiPaths` enum with route values. Paths retain their literal OpenAPI templates, such as `/pets/{id}`.
340
+
341
+ Use an overlay before generation to require properties, allow additional keys, remove
342
+ deprecated members, or declare path parameters. See the
343
+ [overlay examples](../openapi-overlay/README.md#preparing-a-document-for-generation).
275
344
 
276
345
  ```sh
277
- openapi generate zod ./openapi.yaml -o ./src/generated/hotels/zod-schema.ts
278
- # writes src/generated/hotels/zod-schema.ts and src/generated/hotels/types.ts
346
+ openapi overlay openapi.yaml --overlay types.overlay.yaml -o types.openapi.json
347
+ openapi generate typescript -i types.openapi.json -o src/generated
279
348
  ```
280
349
 
281
- | Argument / flag | Type | Default | Description |
282
- | ---------------- | ----------- | ---------- | --------------------------------------------------------- |
283
- | `<input>` | path or URL | (required) | OpenAPI document. |
284
- | `--output`, `-o` | file path | (required) | Output file. `types.ts` is written to the same directory. |
285
- | `--force` | boolean | `false` | Regenerate `types.ts` even when its stamp matches. |
350
+ #### `zod` output (`zod-schema.ts`)
286
351
 
287
- The output imports `zod` and `type { components } from "./types"`. It exports one validator per component schema, typed as `z.ZodType<components["schemas"][Name]>`, plus `<operationId>RequestBody` validators for request bodies:
352
+ The file imports `zod` and `type { components, operations } from "./types"`. It exports one validator per component schema, typed as `z.ZodType<components["schemas"][Name]>`, plus `<operationId>RequestBody` validators for request bodies. During generation, Zod is resolved from the output directory and must be `zod@4.6.7`.
288
353
 
289
354
  ```ts
290
355
  import { Pet } from "./generated/pets/zod-schema";
@@ -292,29 +357,17 @@ import { Pet } from "./generated/pets/zod-schema";
292
357
  const pet = Pet.parse(await response.json()); // typed as components["schemas"]["Pet"]
293
358
  ```
294
359
 
295
- ### `openapi generate tanstack-query <input> -o <dir> --name <name>`
296
-
297
- Generates a typed API client and TanStack Query helpers. Requires `@shipworthy/openapi-tanstack-query` and `@shipworthy/openapi-typescript`.
298
-
299
- ```sh
300
- openapi generate tanstack-query ./openapi.yaml -o ./src/generated --name hotels
301
- # writes src/generated/hotels/client.ts and src/generated/hotels/types.ts
302
- ```
360
+ With `--camel-case`, the validators check camelCase objects. Convert wire payloads to camelCase before you parse them.
303
361
 
304
- | Argument / flag | Type | Default | Description |
305
- | ---------------- | ----------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
306
- | `<input>` | path or URL | (required) | OpenAPI document. |
307
- | `--output`, `-o` | directory | (required) | Directory that will contain `<name>/client.ts` and `<name>/types.ts`. |
308
- | `--name` | string | (required) | Client name. Used as the output subdirectory and, in PascalCase, as the prefix of generated exports. It must start with a letter and contain only letters, numbers, and single `-` or `_` separators (`^[a-zA-Z][a-zA-Z0-9]*(?:[-_][a-zA-Z0-9]+)*$`). |
309
- | `--force` | boolean | `false` | Regenerate `types.ts` even when its stamp matches. |
362
+ #### `tanstack-query` output (`client.ts`)
310
363
 
311
- For `--name hotels`, `client.ts` exports:
364
+ Generates a typed API client and TanStack Query helpers. For `-o src/generated/hotels`, `client.ts` exports:
312
365
 
313
366
  - `createHotelsApiClient(options)` and the `HotelsApiClient` type.
314
367
  - `createHotelsApiQueryUtils(client, options?)`, which returns per-operation helpers (`queryOptions`, `mutationOptions`, `infiniteOptions`) keyed by `operationId`.
315
368
  - Type re-exports `HotelsApiPaths`, `HotelsApiComponents`, and `HotelsApiOperations`.
316
369
 
317
- A `-` or `_` in the name marks a word boundary: `--name my-pets` gives `createMyPetsApiClient`. The command logs `Generated <path>/client.ts`. Operations need unique `operationId`s.
370
+ Operations need unique `operationId`s.
318
371
 
319
372
  ```ts
320
373
  import { useQuery } from "@tanstack/react-query";
@@ -331,24 +384,32 @@ const hotels = useQuery(
331
384
  );
332
385
  ```
333
386
 
387
+ Add `--camel-case` to use `minStars` in app code while sending `min_stars` to the API. The
388
+ generated client also converts JSON request, success, and error bodies according to the schema.
389
+
334
390
  See the `@shipworthy/openapi-tanstack-query` README for the full runtime API.
335
391
 
336
- ### `openapi generate standard-schema <input> [-o <file>]`
392
+ #### `standard-schema` output (`standard-schema.ts`)
337
393
 
338
- Generates [Standard Schema](https://standardschema.dev) validators backed by Effect `Schema`. Requires `@shipworthy/openapi-standard-schema`. This command does not write `types.ts`.
394
+ Generates [Standard Schema](https://standardschema.dev) validators backed by Effect `Schema`. For each component schema, the output exports a type and a value with the same name, e.g. `export type Pet = …` and `export const Pet = Schema.toStandardSchemaV1(…)`. This plugin does not need `types.ts`.
339
395
 
340
- ```sh
341
- openapi generate standard-schema ./openapi.yaml -o ./src/generated/schemas.ts
342
- ```
396
+ #### Migrating from the per-plugin subcommands
343
397
 
344
- | Argument / flag | Type | Default | Description |
345
- | ---------------- | ----------- | ---------- | ------------------------------------------- |
346
- | `<input>` | path or URL | (required) | OpenAPI document. |
347
- | `--output`, `-o` | file path | stdout | Output file. Writes to stdout when omitted. |
398
+ Earlier versions had one subcommand per generator, a positional `<input>`, an output file path for some generators, a `--force` flag, and a `types.ts` cache that skipped regeneration when its stamp matched. These are removed: the input moved to `-i`, `-o` is always a directory, file names are fixed, and every run regenerates all its files. `--check` covers every generated file.
348
399
 
349
- For each component schema, the output exports a type and a value with the same name, e.g. `export type Pet = …` and `export const Pet = Schema.toStandardSchemaV1(…)`.
400
+ | Before | Now |
401
+ | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
402
+ | `openapi generate typescript spec.yaml -o ./schema.ts` | `openapi generate typescript -i spec.yaml -o ./dir` (writes `./dir/types.ts`) |
403
+ | `openapi generate typescript spec.yaml > schema.ts` | `openapi generate typescript -i spec.yaml > schema.ts` |
404
+ | `openapi generate zod spec.yaml -o src/generated/hotels/zod-schema.ts` | `openapi generate zod -i spec.yaml -o src/generated/hotels` |
405
+ | `openapi generate tanstack-query spec.yaml -o src/generated --name hotels` | `openapi generate tanstack-query -i spec.yaml -o src/generated/hotels` |
406
+ | `openapi generate standard-schema spec.yaml -o src/generated/schemas.ts` | `openapi generate standard-schema -i spec.yaml -o src/generated` (writes `standard-schema.ts`) |
407
+ | Several `generate` commands for one SDK | One command: `openapi generate tanstack-query zod standard-schema -i spec.yaml -o src/generated/hotels` |
408
+ | `--force` | Removed. Every run regenerates every file. |
350
409
 
351
- ## Generated file headers and `types.ts` caching
410
+ `--camel-case` now also applies to `zod-schema.ts` and `standard-schema.ts`, so Zod no longer needs a separate directory with API-cased types. `--wire-types` is a general flag rather than a `tanstack-query` option.
411
+
412
+ ## Generated file headers
352
413
 
353
414
  Every generated file starts with a stamped header:
354
415
 
@@ -357,18 +418,13 @@ Every generated file starts with a stamped header:
357
418
  * This file was auto-generated by @shipworthy/openapi-typescript.
358
419
  * Do not make direct changes to the file.
359
420
  *
360
- * @shipworthy/openapi-typescript@0.0.1:<sha256 of the schema contents>
421
+ * @shipworthy/openapi-typescript@0.0.1:<sha256>
361
422
  */
362
423
  ```
363
424
 
364
- The stamp is `<generator package>@<generator version>:<sha256>`. The hash covers the document text, not its location. Reading the same document from stdin, from another path, or from a URL gives the same stamp.
365
-
366
- `generate zod` and `generate tanstack-query` both need `types.ts`:
425
+ The stamp is `<generator package>@<generator version>:<sha256>`. The hash covers the document text. It does not depend on the document's location, so reading the same document from stdin, another path, or a URL gives the same stamp.
367
426
 
368
- - The command regenerates `types.ts` only if the file is missing or its stamp differs from the current one. A stamp changes when the schema contents or the `@shipworthy/openapi-typescript` version change. Otherwise the existing `types.ts` is left as is.
369
- - `--force` always regenerates it.
370
- - These commands always write `types.ts` with the default type flags. To use custom flags (`--enum`, `--root-types`, …), run `openapi generate typescript` yourself.
371
- - The main output (`client.ts`, the Zod file) is always overwritten.
427
+ Files generated with `--camel-case` add a `Field casing: camelCase` line after the stamp, and their hash covers the document text plus the casing setting, so toggling the flag changes the stamp. `wire-types.ts` never has the casing line and always uses the plain hash.
372
428
 
373
429
  ## Programmatic use
374
430
 
@@ -395,29 +451,38 @@ openapi.pipe(
395
451
  );
396
452
  ```
397
453
 
398
- This requires `effect` `^4.0.0` and `@effect/platform-node` `^4.0.0`, which the CLI also depends on. Add them to your own `package.json` if your code imports them directly. No other functions or types are exported. For direct access to the generation pipeline, use `@shipworthy/openapi-generator`, `@shipworthy/openapi-parser`, and `@shipworthy/openapi-transformer`.
454
+ This uses `effect` and `@effect/platform-node` `4.1.4`. The CLI depends on both, but add them to your own `package.json` when your code imports them, using versions compatible with the CLI so they resolve to one copy. No other functions or types are exported. For direct access to the generation pipeline, use `@shipworthy/openapi-generator`, `@shipworthy/openapi-parser`, and `@shipworthy/openapi-transformer`.
399
455
 
400
456
  ## Errors and exit codes
401
457
 
402
458
  Each failure prints an `ERROR` block to stderr and exits with `1`. Common messages:
403
459
 
404
- | Message | Cause |
405
- | -------------------------------------------------------------------- | ------------------------------------------------------------------------ |
406
- | `Globbing is unsupported.` | `<input>` contains `*`. |
407
- | `Missing required flag: --name` / `--output` | A required flag is missing. |
408
- | `Invalid value for flag --name: … --name must start with a letter…` | `--name` does not match the allowed pattern. |
409
- | `Install <package> to load <export>.` (or a module-resolution error) | The generator package for this command is not installed. |
410
- | `Unsupported Swagger version: 2.x. Use OpenAPI 3.x instead.` | The input is a Swagger 2.0 document. |
411
- | `Unsupported OpenAPI version: <v>` | The `openapi` field is not a 3.x version. |
412
- | `NotFound: FileSystem.readFile (<path>)` | The input file does not exist. |
413
- | `Generated types are not up-to-date!` | `generate typescript --check` found that the output file is out of date. |
414
- | `Expected an OpenAPI 3.x document` | `lint` was given something other than an OpenAPI 3.x document. |
460
+ | Message | Cause |
461
+ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
462
+ | `Globbing is unsupported.` | The input contains `*`. |
463
+ | `Missing required argument: plugins` | `generate` was run without a plugin. |
464
+ | `Unknown plugin: <value>. Available plugins: standard-schema, tanstack-query, typescript, zod.` | A `generate` positional is not a known plugin. |
465
+ | `--wire-types requires --camel-case.` | `--wire-types` was passed without `--camel-case`. |
466
+ | `--wire-types requires typescript, zod, or tanstack-query.` | No selected plugin writes `types.ts`. |
467
+ | `--check requires --output.` | `--check` was passed without `-o`. |
468
+ | `--output is required when generating more than one file.` | `-o` was omitted and the run would write more than one file. |
469
+ | `--output must be a directory. Each plugin writes its own file inside it.` | `-o` ends in `.ts`, `.mts`, `.cts`, `.js`, `.mjs`, `.cjs`, or `.json`. |
470
+ | `Invalid value for flag --name: "<value>". Expected: Schema validation failed: --name must start with a letter and contain only letters, numbers, and single hyphen or underscore separators.` | `--name` does not match the allowed pattern. |
471
+ | `Cannot derive a client name from the output directory "<basename>". Pass --name.` | `tanstack-query` without `--name`, and the output directory's basename is not a valid name. |
472
+ | `Generated files are not up-to-date: <file names>` | `generate --check` found generated files that differ from, or are missing in, the output directory. |
473
+ | `Install <package> to load <export>.` (or a module-resolution error) | The generator package for a selected plugin is not installed. |
474
+ | `Unsupported Swagger version: 2.x. Use OpenAPI 3.x instead.` | The input is a Swagger 2.0 document. |
475
+ | `Unsupported OpenAPI version: <v>` | The `openapi` field is not a 3.x version. |
476
+ | `NotFound: FileSystem.readFile (<path>)` | The input file does not exist. |
477
+ | `Expected an OpenAPI 3.x document` | `lint` was given something other than an OpenAPI 3.x document. |
478
+
479
+ When `generate` fails validation or any file fails to generate, nothing is written and the output directory is unchanged.
415
480
 
416
481
  ## Limitations
417
482
 
418
483
  - One input per run. Globs are not supported.
419
- - Only `generate typescript` reads from stdin. Only `generate typescript` and `generate standard-schema` write to stdout.
420
- - Only `generate typescript` supports `--check`. Only `generate zod` and `generate tanstack-query` support `--force`.
484
+ - Only `generate` reads from stdin. It writes to stdout only when the run produces exactly one file.
485
+ - `generate` file names are fixed; `-o` names a directory, not a file.
421
486
  - `lint` does not resolve external `$ref`s. It lints the document text exactly as given.
422
487
  - Lint rules are fixed (Spectral `oas` with the adjustments listed above). Custom rulesets are not supported.
423
488
 
@@ -427,12 +492,11 @@ Each failure prints an `ERROR` block to stderr and exits with `1`. Common messag
427
492
  - Commands:
428
493
  - `openapi overlay <input> --overlay <overlay> -o <output> [--remove-unused-components]`: bundles and upgrades first, then applies actions using the declared Overlay 1.0 or 1.1 version and writes JSON. The optional flag prunes paths without operations and unreachable components after overlaying.
429
494
  - `openapi lint [--strict] <input>`: exits 1 only on errors such as duplicate `operationId`. `--strict` also fails on every `error`-severity `oas` rule and on syntax errors.
430
- - `openapi generate typescript [<input>] [-o file] [--check] [type flags]`: reads stdin when `<input>` is omitted and writes stdout when `-o` is omitted.
431
- - `openapi generate zod <input> -o <file> [--force]`: also writes `types.ts` in the same directory.
432
- - `openapi generate tanstack-query <input> -o <dir> --name <name> [--force]`: writes `<dir>/<name>/client.ts` and `<dir>/<name>/types.ts`.
433
- - `openapi generate standard-schema <input> [-o file]`.
434
- - Each `generate` subcommand needs its optional peer package installed: `@shipworthy/openapi-typescript` (used by typescript, zod, and tanstack-query), `@shipworthy/openapi-zod`, `@shipworthy/openapi-tanstack-query`, and `@shipworthy/openapi-standard-schema`.
435
- - `types.ts` is regenerated only when its header stamp (`pkg@version:sha256(schema)`) changes. Pass `--force` to override.
495
+ - `openapi generate <plugins...> [-i <input>] [-o <dir>] [--name <name>] [--camel-case] [--wire-types] [--check] [type flags]`: one command runs any number of plugins against one document. Plugin order does not matter. Nothing is written if any file fails to generate.
496
+ - `generate` plugin → file in the `-o` directory: `typescript` → `types.ts`, `zod` → `zod-schema.ts`, `tanstack-query` → `client.ts`, `standard-schema` → `standard-schema.ts`. `types.ts` is also written (once) when `zod` or `tanstack-query` is selected. `tanstack-query` names its exports after `--name`, which defaults to the output directory's basename.
497
+ - `--camel-case` applies to every generated file. `--wire-types` (requires `--camel-case` and a plugin that writes `types.ts`) also writes `wire-types.ts` with the API's field names.
498
+ - `generate` reads stdin when `-i` is omitted and writes the single generated file to stdout when `-o` is omitted. `--check` (requires `-o`) writes nothing and exits 1 when any generated file differs or is missing. Every run regenerates every file; there is no cache and no `--force`.
499
+ - Each `generate` plugin needs its optional peer package installed: `@shipworthy/openapi-typescript` (also required by `zod` and `tanstack-query`), `@shipworthy/openapi-zod`, `@shipworthy/openapi-tanstack-query`, and `@shipworthy/openapi-standard-schema`.
436
500
  - Do not edit generated files by hand. Re-run the command instead.
437
501
  - The only programmatic export is `openapi`, an `effect/cli` `Command`.
438
502
 
package/dist/bin.js CHANGED
@@ -13,7 +13,8 @@ import * as NodeServices from "@effect/platform-node/NodeServices";
13
13
  import { ModuleLoader } from "@shipworthy/openapi-generator";
14
14
  import { Effect, Layer } from "effect";
15
15
  import { Command } from "effect/cli";
16
+ import { Digest } from "effect/crypto";
16
17
  import { FetchHttpClient } from "effect/http";
17
18
  import { openapi } from "./cli.js";
18
19
  import { version } from "./version.js";
19
- version.pipe(Effect.flatMap((version) => openapi.pipe(Command.run({ version }))), Effect.provideService(ModuleLoader, { load: (specifier) => import(__rewriteRelativeImportExtension(specifier, true)) }), Effect.provide(Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer)), NodeRuntime.runMain);
20
+ version.pipe(Effect.flatMap((version) => openapi.pipe(Command.run({ version }))), Effect.provideService(ModuleLoader, { load: (specifier) => import(__rewriteRelativeImportExtension(specifier, true)) }), Effect.provide(Layer.mergeAll(NodeServices.layer, FetchHttpClient.layer, Digest.layerWebCrypto)), NodeRuntime.runMain);
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  import { Command } from "effect/cli";
2
- export declare const openapi: Command.Command<"openapi", {}, {}, import("effect/cli/CliError").UserError, import("effect/Crypto").Crypto | import("effect/FileSystem").FileSystem | import("effect/http/HttpClient").HttpClient | import("effect/Path").Path | import("effect/Stdio").Stdio>;
2
+ export declare const openapi: Command.Command<"openapi", {}, {}, import("effect/cli/CliError").UserError, import("effect/crypto/Digest").Digest | import("effect/FileSystem").FileSystem | import("effect/http/HttpClient").HttpClient | import("effect/Path").Path | import("effect/Stdio").Stdio>;
@@ -1,2 +1,19 @@
1
- import { Command } from "effect/cli";
2
- export declare const generate: Command.Command<"generate", {}, {}, import("effect/cli/CliError").UserError, import("effect/Crypto").Crypto | import("effect/Stdio").Stdio | (import("effect/FileSystem").FileSystem | import("effect/http/HttpClient").HttpClient | import("effect/Path").Path)>;
1
+ import { Option } from "effect";
2
+ import { CliError, Command } from "effect/cli";
3
+ export declare const generate: Command.Command<"generate", {
4
+ readonly camelCase: boolean;
5
+ readonly enum: boolean;
6
+ readonly enumValues: boolean;
7
+ readonly conditionalEnums: boolean;
8
+ readonly dedupeEnums: boolean;
9
+ readonly exportType: boolean;
10
+ readonly arrayLength: boolean;
11
+ readonly rootTypes: boolean;
12
+ readonly readWriteMarkers: boolean;
13
+ readonly plugins: readonly string[];
14
+ readonly input: Option.Option<URL>;
15
+ readonly output: Option.Option<string>;
16
+ readonly name: Option.Option<string>;
17
+ readonly wireTypes: boolean;
18
+ readonly check: boolean;
19
+ }, {}, CliError.UserError, import("effect/crypto/Digest").Digest | import("effect/Stdio").Stdio | import("@shipworthy/openapi-generator").GenerateServices>;
@@ -1,6 +1,74 @@
1
- import { Command } from "effect/cli";
2
- import { standardSchema } from "./plugins/standard-schema/plugin.js";
3
- import { tanstackQuery } from "./plugins/tanstack-query/plugin.js";
4
- import { typescript } from "./plugins/typescript/plugin.js";
5
- import { zod } from "./plugins/zod/plugin.js";
6
- export const generate = Command.make("generate").pipe(Command.withDescription("Convert an OpenAPI schema to TypeScript."), Command.withShortDescription("Run a generator plugin"), Command.withSubcommands([typescript, zod, tanstackQuery, standardSchema]));
1
+ import { loadService } from "@shipworthy/openapi-generator";
2
+ import { Clock, Effect, Option, Path, Result } from "effect";
3
+ import { Argument, CliError, Command, Flag } from "effect/cli";
4
+ import { asCliError, schemaSource } from "../input.js";
5
+ import { typeFlags } from "./flags.js";
6
+ import { checkFiles, writeFiles, writeStdout } from "./output.js";
7
+ import { ClientName, planFiles } from "./plan.js";
8
+ import { renderFile } from "./render.js";
9
+ import { readSource, readStdin } from "./source.js";
10
+ const run = Effect.fn("generate")(function* (flags) {
11
+ const path = yield* Path.Path;
12
+ const files = planFiles({
13
+ plugins: flags.plugins,
14
+ output: flags.output,
15
+ outputName: Option.map(flags.output, (output) => path.basename(path.resolve(output))),
16
+ name: flags.name,
17
+ camelCase: flags.camelCase,
18
+ wireTypes: flags.wireTypes,
19
+ check: flags.check,
20
+ });
21
+ if (Result.isFailure(files)) {
22
+ return yield* new CliError.UserError({ cause: files.failure });
23
+ }
24
+ const started = yield* Clock.currentTimeMillis;
25
+ const source = yield* Option.match(flags.input, {
26
+ onNone: readStdin,
27
+ onSome: (input) => Effect.succeed(input),
28
+ });
29
+ const [text, model] = yield* Effect.all([typeof source === "string" ? Effect.succeed(source) : readSource(source), loadService(source)], { concurrency: "unbounded" });
30
+ const directory = path.resolve(Option.getOrElse(flags.output, () => "."));
31
+ const rendered = yield* Effect.forEach(files.success, (file) => renderFile(file, {
32
+ model,
33
+ text,
34
+ directory,
35
+ typeFlags: flags,
36
+ silent: Option.isNone(flags.output),
37
+ }), { concurrency: "unbounded" });
38
+ yield* Option.match(flags.output, {
39
+ onNone: () => Effect.forEach(rendered, (file) => writeStdout(file.contents), { discard: true }),
40
+ onSome: (output) => flags.check
41
+ ? checkFiles(rendered, directory)
42
+ : writeFiles(rendered, {
43
+ directory,
44
+ label: Option.match(flags.input, { onNone: () => "stdin", onSome: (url) => url.href }),
45
+ outputLabel: output,
46
+ started,
47
+ }),
48
+ });
49
+ }, Effect.mapError(asCliError));
50
+ export const generate = Command.make("generate", {
51
+ plugins: Argument.String("plugins").pipe(Argument.withDescription("Generator plugins to run, in any order: typescript, zod, tanstack-query, standard-schema."), Argument.variadic({ min: 1 })),
52
+ input: Flag.String("input").pipe(Flag.withAlias("i"), Flag.withDescription("OpenAPI schema file or URL. Reads stdin when omitted."), Flag.optional, Flag.mapEffect((input) => Option.match(input, {
53
+ onNone: () => Effect.succeed(Option.none()),
54
+ onSome: (value) => Effect.asSome(schemaSource(value)),
55
+ }))),
56
+ output: Flag.String("output").pipe(Flag.withAlias("o"), Flag.withDescription("Directory that receives the generated files. Writes the only file to stdout when omitted."), Flag.optional),
57
+ name: Flag.String("name").pipe(Flag.withDescription("tanstack-query client name: the prefix of generated exports. Defaults to the output directory name."), Flag.withSchema(ClientName), Flag.optional),
58
+ wireTypes: Flag.Boolean("wire-types").pipe(Flag.withDescription("Also write wire-types.ts with API field names (requires --camel-case)."), Flag.withDefault(false)),
59
+ check: Flag.Boolean("check").pipe(Flag.withDescription("Write nothing; fail when a generated file in --output is missing or out of date."), Flag.withDefault(false)),
60
+ ...typeFlags,
61
+ }, run).pipe(Command.withDescription("Generate code from an OpenAPI schema with one or more plugins."), Command.withShortDescription("Generate types, validators, and clients"), Command.withExamples([
62
+ {
63
+ command: "openapi generate typescript -i ./openapi.yaml -o ./src/generated/api",
64
+ description: "Write src/generated/api/types.ts",
65
+ },
66
+ {
67
+ command: "openapi generate zod tanstack-query -i ./openapi.json -o ./src/generated/reservations --camel-case --wire-types",
68
+ description: "Write client.ts, types.ts, wire-types.ts, and zod-schema.ts in src/generated/reservations",
69
+ },
70
+ {
71
+ command: "openapi generate typescript < ./openapi.json > ./types.ts",
72
+ description: "Read stdin and write types.ts to stdout",
73
+ },
74
+ ]));