Skip to content

feat!: v4 — native renderer by default, typed invalidation maps, Bun 1.4.4 - #104

Open
urbankrepel-povio wants to merge 18 commits into
mainfrom
perf/native-default-renderer
Open

urbankrepel-povio wants to merge 18 commits into
mainfrom
perf/native-default-renderer

Conversation

@urbankrepel-povio

@urbankrepel-povio urbankrepel-povio commented Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The 4.0 line, released so far as 4.0.0-rc.1 … 4.0.0-rc.4 on npm (next).

  • The native (Rust) renderer is the default. It is ~1.5× faster than JS on large specs and its output is byte-identical.
  • Typed InvalidationMap callbacks. Types come from the spec, with no any.
  • any is removed from src.
  • builderConfigs is removed.
  • Tooling: Bun 1.4.4, type-checking with tsrs.

Commits

Native renderer

  • perf: make native local-model rendering linear in tag count. O(tags × schemas) became linear. On a 6,400-path spec, compile time went from 3.3 s to 345 ms.
  • perf: render default options fully native. The defaults (tsNamespaces, acl) used to go through the hybrid path, which was ~20× slower than plain JS (12.1 s vs 0.56 s on a 200× petstore spec). Two more super-linear spots are fixed, and Rust emits the default AppAbility file. Every layout now renders fully native.
  • build: publish v4 tags and ship an unstripped native addon. The publish workflow ignored v4.* tags. A stripped dylib fails to load on recent macOS ("mis-aligned LINKEDIT string pool"), so the addon now ships unstripped (~0.4 MB larger).
  • fix: type lazySchemas render helpers with PropertyRefNames. Fixes the merge with Add lazySchemas option for on-demand model namespaces #105.

Typed invalidation maps

  • feat: type InvalidationMap callbacks from the generated mutations. queryModules.ts exports QueryModuleMutations, built from each module's mutation hooks via MutationEffectsOf<typeof Queries.useCreate | …>.
  • feat: type InvalidationMap without naming the mutations, and fix: type bare InvalidationMap and nested mutation variables.
    • The generated file registers the module enum and the mutation types globally. As a result, a bare InvalidationMap and InvalidationMap<QueryModule> are both typed from the spec.
    • data is the union of the module's mutation responses.
    • variables are merged into one object, and fields that only some mutations send become optional at any depth.

Types and cleanup

  • refactor: remove any from src. Real types or unknown everywhere.
  • fix: accept any ErrorHandler in RestClient and NativeRestClient. The clients take ErrorRethrower (exported), so ErrorHandler<"BAD_REQUEST" | …> fits.
  • feat!: remove builder configs generation.

Tooling

  • chore: bump Bun to 1.4.3 and drop unused tsx, then chore: bump Bun to 1.4.4. packageManager, engines, .tool-versions, @types/bun and the CI workflows.
  • chore: type-check with tsrs. It gives the same diagnostics as tsc 7.0 on this repo (tsrs targets TS 7.1). It is ~2× faster here and ~10× faster on a 2,400-file app.

Breaking changes

  • builderConfigs and its options are gone: filterParamName, dataResponseParamNames, dynamicInputsImportPath, dynamicColumnsImportPath, and configs.configs.
  • Invalidation callbacks: callbacks of modules not in the generated QueryModuleMutations receive unknown instead of any. Callbacks of generated modules are typed from the spec, so code that reads fields some responses lack, or destructures the variables of a mutation that has none, now needs narrowing.
  • InvalidationMap's second type parameter is now the mutations map (it was TData). InvalidationMapFunc defaults to unknown.
  • RestClient's interceptors option is RestInterceptor<[]>[], since interceptors are attached without arguments.
  • Bun ≥ 1.4.4 is required for development.

Verification

  • This repo: typecheck and lint are clean. test:ts and test:native pass 1,285 tests each. In the renderer-parity run, JS and native hash identically on 5,324 files across 876 option combinations. cargo test passes.
  • TOS FE: with rc.4 and a one-line fix of a real bug the new types caught (the Quotes callback named its data parameter variables), the FE tsc run has 0 errors.

Known issues (not changed here)

  • generate --input … --output … without a config file generates no endpoints. includeTags appears to resolve to [""].
  • There is no native binary for Intel macOS or Linux arm64; those platforms use the JS renderer.

🤖 Generated with Claude Code

urbankrepel and others added 6 commits October 22, 2026 12:54
render_local_models called render_common_models once per tag, and each call
re-walked every component schema (collect_enum_objects) and rebuilt the
property reference names for all schema refs. The inline-enum import lookup
also scanned all schemas per enum. Together this was O(tags x schemas).

Collect enum objects and property reference names once, apply the per-tag
namespace prefix at lookup time, and index inline enum names by code.

Native compile on a 400x petstore spec (6,400 paths): 3.3 s -> 345 ms.
Output is byte-identical to the JS renderer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the default options (tsNamespaces without modelsInCommon, acl on) the
native renderer was not trusted to be complete, so generation went through
the hybrid path: Rust rendered everything, then JS rendered again. That Rust
render was also super-linear, making the default CLI run ~20x slower than
plain JS (12.1 s vs 0.56 s on a 200x petstore spec).

- resolve_model_namespace_owners: build the model-owner index once per
  render (ModelOwners) instead of per tag module, and rewrite doc-comment
  type references in one pass instead of one replace per owned model.
- render_app_acl: emit the untyped AppAbility file when there are no ACL
  rules, matching the TypeScript renderer.
- Allow every namespace/model layout to render fully native; the
  64-layout parity suite shows identical output.

Default options now render fully native with byte-identical output
(up to 6,804 files); generation on a 400x petstore spec takes ~385 ms
vs ~600 ms in JS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pin Bun 1.4.3 in packageManager, engines, .tool-versions, @types/bun and
the CI workflows. Scripts already run through Bun, so tsx was unused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run `typecheck` with tsrs (oxc-project/tsrs), a Rust port of the TypeScript
7 checker. It reports the same diagnostics as tsc here and is ~1.8x faster
with ~2.2x less memory. The version is pinned exactly while tsrs is pre-1.0.
Binaries exist for macOS arm64 and Linux; elsewhere use `bunx tsc --noEmit`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace every `any` with a real type or `unknown`:

- Runtime library: request `data` is `unknown`, error handlers are
  `ErrorHandler<never>` (as useAclCheck already uses), interceptor tuples
  are `unknown[]`, and RestClient's constructor takes `RestInterceptor<[]>`
  since it attaches them without arguments.
- InvalidationMap/InvalidationMapFunc default to `unknown`; method syntax
  keeps callbacks with annotated parameters assignable.
- Generator and CLI helpers use SchemaResolver, OpenAPIV3 and yargs types;
  the yargs middleware merges the loaded config into argv as yargs would.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The publish workflow only ran for v1-v3 tags. Stripping the release dylib
(`strip = "symbols"`, and `"debuginfo"` too) produces a binary that recent
macOS refuses to load ("mis-aligned LINKEDIT string pool"), which would make
the CLI silently fall back to JS. Ship it unstripped (~0.4 MB larger).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
urbankrepel and others added 2 commits October 22, 2026 14:56
4.0.0-rc.1 typed the clients' errorHandler as ErrorHandler<never>, which
rejects handlers with specific codes such as
ErrorHandler<"BAD_REQUEST" | "UNAUTHORIZED">. The clients only call
rethrowError, so type them with ErrorRethrower (exported), which every
ErrorHandler<Codes> satisfies.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
4.0.0-rc.1 gave invalidation callbacks `unknown` data and variables, so
callbacks like `(data, { officeId, id }) => ...` no longer type-checked.

queryModules.ts now also exports `QueryModuleMutations`: for each module,
`MutationEffectsOf<typeof Queries.useCreate | ...>` over its mutation hooks.
`InvalidationMap<QueryModule, QueryModuleMutations>` types each module's
callback with that module's mutation `data` (union) and `variables` merged
into one object, where keys only some mutations take are optional. Without
the second parameter callbacks keep `unknown`.

Both renderers emit identical output; a new test type-checks a generated
petstore client against a typed map for both renderers and namespace modes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rc.2 typed invalidation callbacks only through
`InvalidationMap<QueryModule, QueryModuleMutations>`, so existing
`InvalidationMap<QueryModule>` declarations still got `unknown`.

The generated queryModules.ts now extends a global
`OpenApiCodegenQueryModuleMutations` interface with QueryModuleMutations,
and InvalidationMap defaults its mutations to it. A global interface,
because interfaces re-exported from the bundled declaration chunks can't
be augmented through the package entry points. useMutationEffects calls
the callback through the untyped InvalidationMapFunc, as the module is
only known at runtime.

Verified against the packed package with InvalidationMap imported from
the root, /query and /config entry points.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
urbankrepel and others added 2 commits October 22, 2026 15:44
Drop the `builderConfigs` option and the `*.configs.ts` files it generated,
with its options (`filterParamName`, `dataResponseParamNames`,
`dynamicInputsImportPath`, `dynamicColumnsImportPath`), the `configs`
output type, the CLI flag and the native renderer's option. They are no
longer used. Helpers only the configs generator used are removed too.

BREAKING CHANGE: `builderConfigs` and its options are gone, and
`configs.configs` is no longer part of the `configs` option.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bare `InvalidationMap` (no type arguments, as the TOS FE declares it)
was keyed by any module key, so it never matched the registered mutation
types and callbacks still got `unknown`.

- queryModules.ts also registers its QueryModule enum globally
  (OpenApiCodegenQueryModules), and InvalidationMap's default module type
  is that enum (RegisteredQueryModule), falling back to any key.
- Variables merge at every depth: request-body fields only some mutations
  send become optional (`variables?.data?.projectId`).
- Common keys are mapped through Extract<keyof T, PropertyKey>; a
  homomorphic `keyof T` mapping distributed over the union and skipped the
  merge.

Checked against the TOS FE's invalidationMap.ts: 16 errors -> 4, all in one
callback that names its first parameter `variables` although it is `data`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
urbankrepel and others added 3 commits October 23, 2026 07:14
packageManager, engines, .tool-versions, @types/bun and the CI workflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merging main brought in #105, whose lazy-schema helpers still took the
property reference names as `&HashMap<&str, String>`. This branch replaced
that map with `PropertyRefNames` (built once per render), so the native
crate didn't compile on the PR's merge ref.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@urbankrepel-povio urbankrepel-povio changed the title perf: render default options natively and type-check with tsrs feat!: v4 — native renderer by default, typed invalidation maps, Bun 1.4.4 Oct 23, 2026
Callback `data` was the union of a module's mutation responses, and
`variables` kept `void` for mutations without variables. Both made
callback parameters unions of distinct types, which strict lint rules
(e.g. TOS's single-value-type check) reject and which forced narrowing
before reading a field.

- `data` merges like `variables` (MergedMutationData): fields only some
  responses have become optional, at any depth.
- `void` becomes `undefined`, which is what callbacks receive for
  mutations without variables or response body.

Checked against TOS: the variable-type check reports 0 violations, FE tsc
and all FE tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Templates left spaces at line ends: JSDoc openers (`/** `), `@property`
lines with an empty description, `mutationFn: (...) => { ` and indented
blank lines. Formatters such as `bun fmt` keep whitespace inside comments,
so consumers saw thousands of whitespace-only lines.

Both renderers' files now go through trimTrailingWhitespace before they
are returned, which also covers descriptions taken from the spec. A test
checks petstore and the configuration fixture with both renderers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a 733-path production spec, the native renderer's output differed
from the TypeScript renderer's in 66 of 614 files with the default
options, and in many more with the frontend options. Both option sets
are now byte-identical.

- Inline enums with a component enum's values are named by that enum
  and listed with the other component enums, after the endpoint
  schemas. Enum codes leave out `null`, both in the resolver's
  dependency edges and when model imports are collected.
- Query and body `@param` docs list every schema constraint, plus the
  media type's example.
- Model imports in namespace mode follow each owner's first use. With
  `modelsInCommon`, imports go through the module's proxy. Queries
  import the native transport types before the upload client.
- Top-level array schemas document their items' properties (`[0].x`).
- Extracted enums keep the owner their component usages gave them. With
  `modelsInModules` they go to each of those modules, and proxies no
  longer re-export enums used only by generated schemas.
- Recursive properties use `z.lazy(() => …)`, like the TypeScript
  renderer since #91. `additionalProperties: false` adds no index
  signature.
- A generated schema's dependencies also follow refs that are already
  registered, and same-valued component enums when `extractEnums` is on.

test/renderer-regressions.yaml reproduces each case with neutral names.
It joins the parity fixtures, so every configuration scenario checks it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`excludeRedundantZodSchemas` (on by default) drops components that no
included operation uses. Some specs carry such schemas on purpose, e.g.
payloads that another app consumes, such as the BE's PDF payloads that a
PDF renderer validates. `includeSchemas` lists component names to
generate anyway, together with the schemas they depend on. It works in
the config file and as a comma-separated `--includeSchemas` flag.

Both renderers support it, and the parity fixture and option matrix
cover it. `resolveConfig` now exports the CLI params type, which the
runner used to duplicate.

The native model proxies also list a component's extracted enums in
extraction order, as the TypeScript renderer does. They used to follow
the order of appearance in the schema code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
v4 — 75ef908d Deployed Oct 23, 2026 by urbankrepel-povio via publish #289
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.

2 participants