Skip to content

feat: openapi typescript client#2885

Open
Marshevskyy wants to merge 95 commits into
mainfrom
feat/ts-client-gen
Open

feat: openapi typescript client#2885
Marshevskyy wants to merge 95 commits into
mainfrom
feat/ts-client-gen

Conversation

@Marshevskyy

@Marshevskyy Marshevskyy commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

What/Why/How?

Adds @redocly/client-generator and the experimental redocly generate-client command: generate a fully-typed TypeScript client from an OpenAPI description — auth, retries, middleware, typed streaming, auto-pagination, and mocks out of the box. Output is built via the TypeScript compiler AST (not string templates), so it is correct by construction; typescript is the only peer dependency (the CLI provides it).

The architecture: one client, two distributions

Generated clients are typed operation descriptors (OPERATIONS … satisfies Record<string, OperationDescriptor>) plus an Ops type, wired into a createClient instance backed by a hand-written, directly-tested runtime (100% coverage). The --runtime option picks how that runtime ships:

  • inline (default) — one self-contained file, zero runtime dependencies (web-standard fetch/AbortController/URLSearchParams), embedding only the runtime modules the API actually uses (no SSE code for a spec without streams).
  • package — the generated file imports the engine from @redocly/client-generator, so runtime fixes reach every consumer via npm update with no regeneration; the emitted satisfies clause doubles as a build-time version-skew guard.

Application code is identical in both modes. Existing tools force a choice between types-only (hand-write every fetch/auth/retry) or a client with a permanent runtime dependency — this makes that a per-project knob instead.

Surface

  • Input: OpenAPI 3.0 / 3.1 / 3.2 + Swagger 2.0.
  • Output: --output-mode single (default) or split (entry + <name>.schemas.ts). Every module exports both call styles — the client instance (grouped args + configure/use/auth) and flat free functions (--args-style shapes them) — plus createClient for per-tenant/multi-instance use.
  • Types: inline schema types, discriminated-union is<Member>() guards, <Op>Result/Params/Body/Variables aliases, optional Date typing, typed multipart (binary → Blob).
  • Runtime: auth from securitySchemes (async token providers, per-instance credentials, generated setBearer/setApiKey* sugar); composable middleware where ctx.operation.{id,path,tags} are literal unions from your spec — a misspelled operationId fails compilation; opt-in abort-aware retries (backoff + jitter + Retry-After); per-call parseAs; OpenAPI query-serialization styles; --error-mode result for { data, error, response }; typed Server-Sent Events (auto-reconnect, Last-Event-ID, OAS 3.2 itemSchema).
  • Auto-pagination: declared, never guessed — a client.pagination convention block in redocly.yaml (or the x-pagination extension) gives paginated operations typed .pages() / .items() async iterators (cursor/offset/page styles), with item types resolved statically from the response schema. The convention applies only where it structurally fits (verified against params and the response schema); an explicit rule that doesn't fit fails generation with a per-operation error.
  • Publisher defaults: --setup bakes a defineClientSetup({ config, middleware }) module into the client, layered between spec defaults and app configure().
  • Add-on generators (--generators): sdk (default), zod, tanstack-query (React/Vue/Svelte/Solid), swr, transformers, mock (MSW, baked or faker) — each emits its own file and adds no dependency to the client — plus an experimental defineGenerator plugin API for custom emitters.
  • Config: CLI flags or a client block in redocly.yaml (shared defaults + per-API overrides under apis.<name>.client); programmatic generateClient(...) API.
  • Hardened: safe-identifier coercion, comment escaping, globalThis-safe embedded types, bounded SSE reader, restricted server-URL schemes, output-path and setup-path validation.

Size, honestly

~91k added lines across 433 files, but the implementation is a small fraction:

Area Lines
Implementation (packages/client-generator/src, excl. tests) ~9,200
Unit tests ~15,400
E2E tests + live-server consumers ~17,200
17 runnable examples (mostly committed generated clients) ~45,500
User docs + ADRs (0017/0018) ~1,200
CLI/core wiring (command, config schema) ~500

Reference

Testing

  • npm test green (compile + typecheck + unit + e2e); 100% per-file coverage on the package.
  • e2e generates clients, type-checks them under strict tsc, and runs them against a mock server.
  • Runnable, drift-checked examples under packages/client-generator/examples/.

Screenshots (optional)

Check yourself

  • This PR follows the contributing guide
  • All new/updated code is covered by tests
  • Core code changed? - Tested with other Redocly products (internal contributions only)
  • New package installed? - Tested in different environments (browser/node)
  • Documentation update has been considered

Security

  • The security impact of the change has been considered
  • Code follows company security practices and guidelines

Note

Medium Risk
Large new surface area (codegen + runtime + CLI) with experimental API stability, though it is heavily tested and isolated behind a new command and package.

Overview
Introduces @redocly/client-generator and the experimental redocly generate-client command to emit typed TypeScript clients from OpenAPI (3.x and Swagger 2.0), with optional companion outputs (Zod, TanStack Query, SWR, transformers, MSW mocks) and a defineGenerator plugin hook.

The CLI wires handleGenerateClient: resolve jobs from redocly.yaml (client / apis.<name>.client / clientOutput) or a path/alias, merge flags over config, bundle via the same per-alias config as lint/bundle, then call generateClient. Notable flags include --runtime (inline zero-dep embed vs package import), --output-mode (single / split), --setup, and repeated --generator.

@redocly/openapi-core gains a loosely typed top-level client block and per-API clientOutput in the config schema. CI splits e2e into two Vitest shards after the generate-client suites grew large. Docs, README, changeset, formatter/linter ignores for committed generated e2e artifacts, and CLI build banner shims for bundled TypeScript complete the integration.

Reviewed by Cursor Bugbot for commit 698d123. Bugbot is set up for automated code reviews on this repo. Configure here.

…enAPI client

Add `@redocly/openapi-typescript` and the `redocly generate-client` command:
generate a typed TypeScript client from an OpenAPI description. The emitted
client has zero runtime dependencies (web-standard fetch/AbortController/
URLSearchParams) and is produced via the TypeScript compiler AST, so output is
correct by construction; `typescript` is the only peer dependency.

Input: OpenAPI 3.0/3.1/3.2.0 + Swagger 2.0 (normalized to 3.x); file, URL, or a
redocly.yaml `apis:` alias; operationId synthesized when absent.

Output: single / split / tags / tags-split layouts; `functions` or
`service-class` facade (per-instance config + credentials); flat or grouped
argument styles.

Types: inline types; enums as unions or runtime const objects; discriminated-
union `is<Member>()` guards; `<Op>Result/Error/Params/Body/Headers/Variables`
aliases with collision suppression; JSDoc from validation keywords; optional
`Date` typing; typed multipart bodies (binary → Blob) auto-serialized to FormData.

Runtime: setBaseUrl + typed ClientConfig; composable middleware (onRequest/
onResponse/onError); opt-in abort-aware retries (backoff, jitter, Retry-After,
custom retryOn); per-call parseAs; OpenAPI query-serialization styles;
`--error-mode result` discriminated returns; minification-safe OPERATIONS map;
typed Server-Sent Events (async iterators, auto-reconnect, OAS 3.2 itemSchema).

Auth: Basic / Bearer / apiKey (header, query, cookie) from securitySchemes, async
token providers, and per-instance credentials via ClientConfig.auth.

Generators (--generators): sdk (default), zod, tanstack-query (react/vue/svelte/
solid), swr, transformers, mock (MSW handlers + baked or faker data, seedable),
plus an experimental custom-generator plugin API (@redocly/openapi-typescript/
plugin) with dual loading (inline + import specifier) and a validated
compatibility contract. Each generator declares requires/facades/errorModes/
dateTypes, validated up front.

Configuration via CLI flags, a redocly.yaml `x-openapi-typescript` block, or a
defineConfig file; plus `--watch`. Hardened: document-derived names coerced to
safe unique identifiers, comment text escaped, bounded SSE reader. Architecture,
ADRs (0001-0012), and runnable examples included.
@Marshevskyy Marshevskyy requested review from a team as code owners June 16, 2026 07:24
@changeset-bot

changeset-bot Bot commented Jun 16, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 698d123

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@redocly/client-generator Minor
@redocly/openapi-core Minor
@redocly/cli Minor
@redocly/respect-core Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment thread packages/cli/src/commands/generate-client.ts Outdated
Comment thread packages/cli/src/commands/generate-client.ts Outdated
Comment thread .changeset/openapi-typescript.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread README.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread packages/client-generator/src/index.ts
Comment thread packages/client-generator/src/emitters/runtime.ts Outdated
Co-authored-by: Jacek Łękawa <164185257+JLekawa@users.noreply.github.com>
Comment thread packages/client-generator/src/emitters/runtime.ts Outdated
Comment thread packages/client-generator/src/generators/index.ts
Comment thread packages/client-generator/src/index.ts
JLekawa and others added 3 commits June 18, 2026 12:17
…ing docs

Switch the runnable examples and the cafe e2e fixture/snapshot to the spec's
`servers[0].url` (api.cafe.redocly.com), regenerated with the current generator.
Demonstrate middleware in the fetch-functions example via `onResponse` so it
isn't blocked by the demo API's CORS preflight (it doesn't allow a custom
`X-Request-Id` request header). Add a "Testing the generated client" section to
the README (Node / browser-CORS / MSW mocks).
Comment thread packages/client-generator/src/generators/resolve.ts
Resolve conflicts in package.json, package-lock.json, packages/cli/package.json,
tsconfig.json, tsconfig.build.json, and vitest.config.ts.

- Adopt main's esbuild-bundled CLI build; add @redocly/openapi-typescript to
  packages/cli devDependencies so it bundles alongside openapi-core/respect-core.
- Bump openapi-typescript's @redocly/openapi-core dependency 2.31.4 -> 2.34.0 to
  match the workspace, so npm symlinks the workspace package instead of a nested
  copy (the mismatch produced divergent Config type identities).
- Extend the CLI bundle banner to shim __filename/__dirname (via var, to coexist
  with deps that self-declare them) so the bundled typescript compiler used by
  generate-client runs in ESM scope.
- Keep the per-glob 100% coverage threshold for openapi-typescript alongside
  main's repo-wide branches:73.
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Performance Benchmark (Lower is Faster)

CLI Version Bundle Lint Check Config
cli-latest ▓ 1.01x ± 0.01 ▓ 1.02x ± 0.01 ▓ 1.00x ± 0.01
cli-next ▓ 1.00x (Fastest) ▓ 1.00x (Fastest) ▓ 1.00x (Fastest)

Comment thread packages/client-generator/src/emitters/runtime.ts Outdated
Comment thread packages/client-generator/src/generators/resolve.ts
…MD009

Three prose lines had a stray single trailing space (lines 644, 651, 939),
which markdownlint MD009 rejects (expects 0 or 2). Verified clean with
markdownlint-cli2 v0.22.0 against the repo's .markdownlint.yaml.
Comment thread packages/cli/src/commands/generate-client.ts Outdated
The vale job used reviewdog with filter_mode: file, which fetches the PR
diff to scope findings to changed files. GitHub's diff API caps at 20000
lines, so large PRs (this one adds ~54k lines) return 406 and reviewdog
fails on the post step — not on any actual vale finding.

filter_mode: nofilter skips the diff fetch and lints the full files
directly. Verified the committed docs are clean (0 errors/warnings/
suggestions across 320 files with vale 3.15.1), so nofilter adds no noise
and the error-level gate still holds.
The consumer harnesses (base/cafe/sse) have tracked index*.ts that import a
generated `./api.js`. The repo-wide `tsc --noEmit` includes tests/**/*.ts, so
it typechecks those imports — but `api.ts` was gitignored and only created when
the e2e suite ran, so a fresh checkout (CI) failed with TS2307.

The harnesses already expected api.ts present for typecheck (see the note in
sse.runtime.test.ts); gitignoring it was the gap. generate-client output is
byte-deterministic for these fixtures (fixed ports, fixed specs — verified by
regenerating and diffing), so commit the files instead of touching tsconfig.
The e2e suite still regenerates them each run, producing identical content
(verified: no drift after running base.test.ts).
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 74.21% (🎯 69%) 9840 / 13259
🔵 Statements 74.53% (🎯 69%) 10557 / 14163
🔵 Functions 78.57% (🎯 73%) 2032 / 2586
🔵 Branches 68.26% (🎯 61%) 7160 / 10488
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/cli/src/commands/generate-client.ts 0% 0% 0% 0% 46-207
packages/client-generator/src/config-file.ts 100% 88.88% 100% 100%
packages/client-generator/src/errors.ts 100% 100% 100% 100%
packages/client-generator/src/index.ts 94.11% 90% 100% 94.11% 137-138
packages/client-generator/src/loader.ts 100% 100% 100% 100%
packages/client-generator/src/plugin.ts 100% 100% 100% 100%
packages/client-generator/src/runtime-contract.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/auth.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/client.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/descriptor.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/faker.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/identifier.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/inline-runtime.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/jsdoc.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/mock.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/operation-aliases.ts 93.33% 84.31% 75% 93.02% 217, 228, 239
packages/client-generator/src/emitters/operation-signature.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/operation-types.ts 98.11% 94.44% 100% 97.91% 144
packages/client-generator/src/emitters/operations.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/package-client.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/pagination.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/runtime-sources.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/sample.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/setup-bake.ts 100% 96.15% 100% 100%
packages/client-generator/src/emitters/sse.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/support.ts 60% 100% 60% 75% 7
packages/client-generator/src/emitters/swr.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/tanstack-query.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/transformers.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/ts.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/type-guards.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/types.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/wrapper-support.ts 100% 100% 100% 100%
packages/client-generator/src/emitters/zod.ts 100% 100% 100% 100%
packages/client-generator/src/generators/index.ts 100% 100% 100% 100%
packages/client-generator/src/generators/mock.ts 100% 100% 100% 100%
packages/client-generator/src/generators/resolve.ts 100% 100% 100% 100%
packages/client-generator/src/generators/sdk.ts 100% 100% 100% 100%
packages/client-generator/src/generators/swr.ts 100% 100% 100% 100%
packages/client-generator/src/generators/tanstack-query.ts 100% 100% 100% 100%
packages/client-generator/src/generators/transformers.ts 100% 100% 100% 100%
packages/client-generator/src/generators/zod.ts 100% 100% 100% 100%
packages/client-generator/src/intermediate-representation/build.ts 100% 100% 100% 100%
packages/client-generator/src/intermediate-representation/normalize-swagger2.ts 100% 100% 100% 100%
packages/client-generator/src/intermediate-representation/sanitize-identifiers.ts 100% 100% 100% 100%
packages/client-generator/src/runtime/auth.ts 100% 100% 100% 100%
packages/client-generator/src/runtime/create-client.ts 100% 100% 100% 100%
packages/client-generator/src/runtime/errors.ts 100% 100% 100% 100%
packages/client-generator/src/runtime/index.ts 100% 100% 100% 100%
packages/client-generator/src/runtime/multipart.ts 100% 100% 100% 100%
packages/client-generator/src/runtime/paginate.ts 100% 100% 100% 100%
packages/client-generator/src/runtime/parse.ts 100% 100% 100% 100%
packages/client-generator/src/runtime/retry.ts 100% 100% 100% 100%
packages/client-generator/src/runtime/send.ts 100% 100% 100% 100%
packages/client-generator/src/runtime/setup.ts 100% 100% 100% 100%
packages/client-generator/src/runtime/sse.ts 100% 98.63% 100% 100%
packages/client-generator/src/runtime/url.ts 100% 100% 100% 100%
packages/client-generator/src/writers/index.ts 100% 100% 100% 100%
packages/client-generator/src/writers/single-file-writer.ts 100% 100% 100% 100%
packages/client-generator/src/writers/split-writer.ts 100% 100% 100% 100%
packages/client-generator/src/writers/util.ts 100% 100% 100% 100%
packages/core/src/types/redocly-yaml.ts 93.13% 84.9% 100% 92.92% 467, 499, 505, 549-556, 558, 717-722, 725-730
Generated in workflow #10688 for commit 698d123 by the Vitest Coverage Report Action

Comment thread packages/cli/src/commands/generate-client.ts Outdated
…apshots

The branch added `react`/`react-dom` `^18.2.0` to the root devDependencies
(main has neither — it resolves react 19.2.7 via packages/cli's
`^17 || ^18.2.0 || ^19.2.7` range). That root `^18.2.0` cap forced npm to
hoist react 18.3.1 for the whole workspace, so build-docs rendered React 18's
`useId` format (`tab:R9pq:0`) instead of the React 19 format (`tab_R_9pq_0`)
the committed redoc-static snapshots were generated with — failing
build-docs.test.ts on CI.

Bump root react/react-dom to `^19.2.0` so npm resolves 19.2.7, matching main.
Verified: build-docs.test.ts (7/7) and the react-19 consumers
(tanstack-query.runtime, swr) all pass.
filter_mode: nofilter alone wasn't enough — the github-pr-annotations reporter
still fetches the PR diff to position comments, and GitHub caps that diff at
20000 lines, so this 54k-line PR gets a 406 and reviewdog exits 1 (on the diff
fetch, not on any vale finding).

Switch reporter to `local`: reviewdog prints findings to the job log and exits
non-zero only on vale errors, with no GitHub API call and therefore no diff to
fetch. The gate still holds (committed docs verified: 0 vale errors across 320
files). Trade-off: findings show in the Actions log rather than as inline PR
annotations — which a 20k-line-capped diff can't render on a PR this size anyway.

@tatomyr tatomyr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked a couple of root files. Haven't checked any actual implementation yet.

Comment thread .github/workflows/docs-tests.yaml Outdated
Comment thread docs/@v2/commands/generate-client.md Outdated
Comment thread CONTRIBUTING.md Outdated
Comment thread vitest.config.ts Outdated
Comment thread tsconfig.json Outdated
Comment on lines +26 to +27
"exclude": ["node_modules"],
"include": [

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated a bit, but we have to exclude examples since they include react code, which is not covered in this tsconfig...

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's discuss offline whether we need React examples.

Comment thread packages/cli/scripts/build.mjs
Comment thread packages/openapi-typescript/README.md Outdated
@Marshevskyy Marshevskyy requested a review from tatomyr July 8, 2026 15:01
Several e2e suites each spawn a `tsc --noEmit` gate, a live server, and
multiple `tsx` consumers (~1 GB apiece). Unbounded on the 4-core CI runner
they pile up past its memory ceiling and the agent is OOM-killed, which
GitHub surfaces as a "The operation was canceled" E2E step. Run at most two
test files at once, which stays under the ceiling.
…geCall 204 fix

These generated fixtures embed the inline client runtime, including
`pageCall`. The 204/void fix changed that runtime but only the byte-asserted
`examples/` fixtures were regenerated; these two consumer fixtures are not
byte-checked, so they were left one fix stale. Regenerate them to match.
Comment thread packages/client-generator/src/config-file.ts
…fig blocks

mergeConfig shallow-merged the top-level and per-API `client` blocks, so a
per-API `pagination` override carrying only `operations` (or any partial
field) replaced the whole top-level object, dropping shared convention fields
like `style`, `cursorParam`, and `items`. Layer the `pagination` block instead:
convention scalars survive a partial override, and `operations` merge by id.
Comment thread .github/workflows/tests.yaml Outdated
Comment thread packages/client-generator/src/runtime/sse.ts Outdated
Comment thread package.json Outdated
'\n' +
`❌ Failed to generate TypeScript client${job.name ? ` for ${job.name}` : ''}.\n ${message}\n` +
' Check the API description file path and that the OpenAPI document is valid.'
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catch-all error message misleads for non-file errors

Medium Severity

The catch block wraps every error from generateClient with a trailing hint: "Check the API description file path and that the OpenAPI document is valid." This message is misleading for errors unrelated to file loading — such as generator compatibility failures, pagination validation errors, NotSupportedError from an invalid setup path, or a custom-generator plugin that fails to import. The misleading suffix sends the user debugging in the wrong direction.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6787759. Configure here.

Comment thread packages/client-generator/src/config-file.ts
Comment thread .github/workflows/tests.yaml Outdated
Comment thread packages/client-generator/src/intermediate-representation/build.ts Outdated
Running the whole e2e suite in one step was cancelled mid-run by the GitHub
Actions service once the generate-client suites grew past ~28 — the runner
stayed healthy the whole time (no memory/disk/CPU/fd exhaustion, job token
still renewing), so this was a service-side cancellation of an oversized run,
not a failing test. Split e2e into a 2-shard matrix (`--shard`) so each runner
carries roughly half the suites, well under that threshold.
@Marshevskyy Marshevskyy force-pushed the feat/ts-client-gen branch from 26183bc to be92b1e Compare July 9, 2026 06:38
// A transport failure (connect/DNS/reset) when opening the request, or a mid-stream
// read error, is a dropped connection: fall through to backoff/reconnect when enabled.
if (!reconnect) throw error;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SSE JSON errors reconnect forever

Medium Severity

When an SSE event uses JSON data, parseSseFrame calls JSON.parse without handling failures. A syntax error is not an ApiError, so the outer sse loop treats it like a dropped connection and reconnects when reconnect is enabled, which can repeat indefinitely on a stable bad payload instead of surfacing a parse error.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit be92b1e. Configure here.

Comment thread packages/cli/src/index.ts Outdated
)
.command(
'generate-client [api]',
'Generate a TypeScript client from an OpenAPI description.',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'Generate a TypeScript client from an OpenAPI description.',
'Generate a TypeScript client from an OpenAPI description [experimental].',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

corrected

@tatomyr tatomyr left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's hard to review this at once. Will make another pass later.

Comment thread packages/core/src/types/redocly-yaml.ts Outdated
@@ -308,6 +308,7 @@ const createConfigRoot = (nodeTypes: Record<string, NodeType>): NodeType => ({
...nodeTypes.rootRedoclyConfigSchema.properties,
...ConfigGovernance.properties,
apis: 'ConfigApis', // Override apis with internal format
client: 'Client', // generate-client shared defaults

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please avoid comments. If the name is not descriptive enough -- let's change it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed comment

Comment thread packages/core/src/config/types.ts

- name: E2E Tests
run: npm run e2e
e2e:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason for this change? The e2e job isn't a required check in the repo's settings, and it would be possible to merge a PR even with the job failing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We had canceled GH job without any reason. I tried to do deep research, this is a summary:

Tets started failing after we added more tests, there is no specific test impacting this. Only number of tests...
AI made some research:
What I ruled out with live in-job instrumentation (every metric healthy at the moment of cancel): RAM (~14 GB free), disk (~90 GB free), CPU/load, processes (~190), threads (~480), file descriptors (~2 500 of 65 536), sockets/TIME-WAIT, inodes (18 M free), kernel OOM (none). Also ruled out: parallel contention (serial --maxWorkers=1 still cancels, just later), network, log volume, and any single test (all pass locally and individually).

The decisive piece — from the runner's own _diag logs: the job token renews successfully every 60 s and console uploads succeed (1/1) right up to the cancel. So the runner stays healthy and connected; GitHub's service is deliberately cancelling a healthy job. The exact service-side reason isn't observable from inside — when the service cancels, it tears the step down and stops accepting console output, so streamed diag lines can't upload. That last mile needs GitHub support or org runner-group admin visibility

Comment thread vitest.config.ts Outdated
@@ -24,6 +25,13 @@ const configExtension: { [key: string]: ViteUserConfig } = {
functions: 73,
statements: 69,
branches: 61,
// The hand-written client runtime is held to 100% (aggregate 100% ⇒ every file 100%).
'packages/client-generator/src/runtime/**/*.ts': {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point of having separate thresholds for this package?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, will remove it, since AI added it back 😞

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

corrected

Comment thread .changeset/client-generator.md Outdated

Added an **experimental** `generate-client` command that generates a typed TypeScript client from an OpenAPI description — auth, retries, middleware, typed SSE streaming, and multipart out of the box, built on a hand-written, directly-tested runtime.

Generated clients are typed operation descriptors (`OPERATIONS … satisfies Record<string, OperationDescriptor>`) plus an `Ops` type, wired into a `createClient` instance. Every generated module exports both call styles: the `client` instance (grouped-args methods plus `configure`/`use`/`auth`) and free-function one-liners (`--args-style` shapes them), and re-exports `createClient` for additional per-tenant instances.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave a link to the docs instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

corrected

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can still see the huge changeset. Please focus of what's added leaving behind the configuration intricancies.

Comment thread docs/@v2/commands/generate-client.md Outdated

## Configuration

Instead of passing flags every time, keep the settings in `redocly.yaml` under a top-level `client` block and per-API `apis.<name>.client` / `clientOutput`. Settings resolve **top-level `client` → per-API `client` → CLI flags**. See [`client` configuration](../configuration/reference/client.md) for the full reference.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Settings resolve top-level client → per-API client → CLI flags.

this doesn't seem right. CLI args take precedence over config, and the config is resolved a bit differently (apis-level options override the root ones, as described here, or override the root ones completely -- depends on what we decide).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

corrected

apis:
cafe:
root: ./openapi.yaml # the input
clientOutput: ./src/api/client.ts # optional; defaults to `cafe.client.ts`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does it know the default?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the default output filename is derived from the API's alias (the key under apis:). For the api aliased cafe, when clientOutput is omitted the client is written to .client.ts (i.e. cafe.client.ts) in the redocly.yaml directory. In the command that's fileNameFor(job.name) → ${name}.client.ts where name is the alias.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed to clientOutput: ./src/api/client.ts # optional; defaults to <api-name>.client.ts (here cafe.client.ts), this makes that explicit.


## Precedence

Settings resolve **top-level `client` → per-API `client` → CLI flags** (later wins). A plain file-path invocation ignores `apis:` and uses only the top-level `client`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This section should be only about the config application. No need to mention the full resolution path as already have it here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

corrected

Comment thread packages/cli/src/index.ts Outdated
`\n❌ --output can't target multiple APIs. Set \`clientOutput\` under each api in redocly.yaml, or pass a single <api>.\n`
);
}
for (const [name, apiCfg] of Object.entries(apisCfg)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like you should have been utilising getFallbackApisOrExit to apply the same approach other commands use instead (like in the bundle command, for example).

… pagination examples

`generateClient` is a named export, but the custom-generator snippet imported it as a
default — corrected to `import { generateClient }`. Also add the `pagination`,
`custom-pagination`, and `nested-facade` examples to the Examples list, which had gone stale.
output: outputPath,
config,
configDir,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per-api config not applied

Medium Severity

generateClient is always called with the root config, while other API commands resolve per-alias settings via config.forAlias. Per-API extends, resolve, decorators, and related apis overrides therefore do not apply when bundling the OpenAPI input, so generated clients can diverge from what bundle or lint produce for the same alias.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 0ef1ba1. Configure here.

const contentType = response.headers.get('content-type') ?? '';
if (contentType.toLowerCase().includes('json')) return response.json();
if (contentType.startsWith('text/')) return response.text();
return response.blob();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Text content-type case ignored

Medium Severity

Auto response decoding lowercases the content type only for the JSON check, then compares text/* with a case-sensitive startsWith. A Text/Plain or similar header therefore falls through to blob() instead of text(), so callers get a Blob where a string is expected.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 45887e6. Configure here.

// `errorMode` is fixed at generate time (it shapes the static types); flipping it at
// runtime would silently desync return shapes from `Client<Ops>`, so it is ignored.
const { errorMode: _fixed, ...rest } = next;
Object.assign(config, rest);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Configure replaces entire auth object

Medium Severity

configure() shallow-assigns into the live config, so configure({ auth: { bearer: … } }) replaces the whole auth object. Credentials previously set via client.auth.apiKey / basic are dropped, unlike the auth setters which merge into existing credentials.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 45887e6. Configure here.

Runtime correctness:
- SSE: throw a distinct SseParseError on malformed JSON `data` so a stable bad
  payload surfaces instead of reconnecting forever; detect frame boundaries with
  any two consecutive line terminators (mixed CR/LF), not just matching pairs.
- Security: apply exactly one OR-alternative (the first fully-injectable
  requirement object) so "bearer OR apiKey" never sends both credentials.
- Response decoding: match the content-type case-insensitively so `Text/Plain`
  reads as text, not a Blob.
- configure({ auth }) merges into existing credentials (like the auth setters)
  instead of replacing the whole auth object.
- mergeConfig unions pagination `exclude` across config layers (additive, like
  `operations`) rather than replacing it.

CLI & tooling:
- Drop the misleading "check the file path" suffix from the generate-client
  failure message (it hid generator/pagination/setup errors).
- Remove the racy client-generator runtime lint-staged glob; runtime-sources is
  regenerated on install (`prepare`) and guarded by the drift test.

Docs & config:
- Correct the settings-resolution wording (CLI flags win; per-API overrides the
  top-level field by field), trim the config-reference precedence section, and
  clarify the clientOutput default.
- Shorten the changeset intro to a docs link.
- Drop redundant inline comments in the config node types.
…s config

- Replace the comma-separated `--generators` flag with an idiomatic repeatable
  array option — `--generator sdk --generator zod` (like `--skip-rule`), dropping
  the custom comma coerce. The `generators` config field is unchanged. Updated the
  compatibility-error hint, the e2e suites, and the docs.
- Resolve each api's config with `config.forAlias(alias)` before generating, so
  the OpenAPI input is bundled with that api's `extends` / `resolve` / decorators
  (matching `bundle`/`lint`). Previously the root config was always used, so
  per-API overrides never reached the generated client.
const prepared = await prepareRequest(config, op, args, init, caps);
const opCtx: OperationContext = { id: op.id, path: op.path, tags: [...(op.tags ?? [])] };
yield* stream(config, opCtx, prepared.url, prepared.init, op.sseDataKind ?? 'text');
})();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SSE auth stale on reconnect

Medium Severity

TokenProvider auth is resolved once in prepareRequest before the SSE generator starts, then reconnects reuse those frozen headers and URL query credentials. A refresh-style provider therefore keeps sending the expired token after a drop, while config.headers() still runs per send attempt — so long-lived streams can fail with 401 after reconnect instead of picking up a fresh credential.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f649e42. Configure here.

…w-comment changes

- Update the two validateGenerators unit assertions to the repeatable `--generator`
  hint (missed in the flag rename).
- Regenerate the cafe e2e snapshot for the runtime changes (case-insensitive
  content-type, configure() auth merge).
- Fix a lingering `--generators` reference in the --date-type help text and ARCHITECTURE.md.
…untime changes

The base/cafe/pagination/sse consumer clients embed the inline runtime, so refresh
them for the SSE, content-type, and configure()-auth fixes. (The package-runtime and
tanstack consumers import the runtime, so they're unaffected.)
yield page;
const pageItems = resolvePointer(page, spec.items);
if (!Array.isArray(pageItems) || pageItems.length === 0) return;
position += spec.style === 'page' ? 1 : pageItems.length;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Offset pagination string concatenation

Medium Severity

Offset/page iteration advances with += on a value taken from params without coercing to a number. QueryValue allows strings, so a string page/offset (common from URL or form input) concatenates instead of adding, producing wrong query values and potentially never-ending iteration.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4f9bde4. Configure here.

const value = await resolveToken(provider);
if (scheme.in === 'header') headers[scheme.name] = value;
else if (scheme.in === 'query') query[scheme.name] = value;
else cookies.push(`${scheme.name}=${value}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cookie auth values left unencoded

Medium Severity

Cookie apiKey credentials are written into the Cookie header with raw interpolation. Keys containing ;, =, spaces, or other reserved cookie characters produce a malformed header, so authentication fails silently for otherwise valid credentials.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4f9bde4. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

There are 10 total unresolved issues (including 8 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 698d123. Configure here.

const basic = config.auth?.basic;
if (basic !== undefined) {
headers.Authorization = `Basic ${btoa(`${basic.username}:${basic.password}`)}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basic auth breaks on Unicode

High Severity

resolveAuth builds the Basic header with btoa on the raw username:password string. btoa only accepts Latin-1, so non-ASCII credentials throw InvalidCharacterError before the request is sent, and the same failure is baked into inline clients.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 698d123. Configure here.

...authed.headers,
...stringHeaders(headers),
...(init.headers as Record<string, string> | undefined),
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Headers objects are dropped

High Severity

Per-call and config headers are merged with object spread while casting to a plain record. RequestInit.headers may be a Headers instance or a string[][], and spreading those values contributes no entries, so caller headers are silently ignored.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 698d123. Configure here.

Comment on lines +21 to +25
// Avoid errors when external dependencies use CJS syntax. The bundled
// `typescript` compiler (used by generate-client) references `require`,
// `__filename`, and `__dirname` at runtime — none exist in ESM scope. Shim
// them per output file. `var` (not `const`) so these coexist with the
// self-declared `var __dirname` some deps already inline for ESM compat.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original comment was clear enough and easy to read.

'query-framework'?: 'react' | 'vue' | 'svelte' | 'solid';
'mock-data'?: 'baked' | 'faker';
'mock-seed'?: number;
// Repeated `--generator` flags: built-in names, inline custom-generator names, or plugin

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add that to the docs / yargs descriptions instead of comments.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, avoid comments in this file. It belongs to the CLI package, it represents the entry point and the CLI interface. The code here should be easy to read, and it's should follow the same concepts as the other commands (like lint, bundle, etc.).

* `//host` values are rejected too: the value is inlined as the client's default fetch base.
*/
function isValidServerUrl(value: string): boolean {
if (value.startsWith('/')) return !value.startsWith('//'); // root-relative, not protocol-relative

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit cryptic to read. I believe it's better to explicitly return every separate case.

};

const perApiJob = (name: string): Job => {
const apiCfg = apisCfg[name];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should use config.forAlias(alias) like in other commands (bundle, lint, etc.).

@@ -345,6 +348,55 @@ const ConfigHTTP: NodeType = {
},
};

// `generate-client` settings. Shared defaults live under the top-level `client` key;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comments are needless. Please remove them.

@@ -272,18 +272,34 @@ export type ResolveConfig = {

export type Telemetry = 'on' | 'off';

/**

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's avoid excessive comments as they make the code harder to read. And they appear to be needless.

Comment thread packages/cli/src/index.ts
type: 'string',
})
.options({
output: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have this validation must end in .ts added earlier, like on arguments level ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants