npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cogs/kubb-transforms

v0.4.0

Published

jscodeshift-based code transforms for maintaining Kubb v5-generated codegen output

Readme

@cogs/kubb-transforms

Post-generation codemods for Kubb v5 output.

Kubb generates a correct but deliberately minimal data layer. This package closes the gap between "compiles" and "production-ready": it points Kubb's bundled fetch runtime at @cogs/fetch-client, seeds placeholderData, adds Next.js cache tags via @cogs/react-query's nextHashKeys, opens the four mutation lifecycle seams Kubb emits none of, and names the repeated error type.

It runs as a single output.postGenerate command, so a failing transform fails kubb generate itself and CI can never publish half-transformed codegen.

Targets Kubb 5 only (kubb, @kubb/plugin-fetch, @kubb/plugin-react-query, @kubb/plugin-ts, @kubb/plugin-zod); there is no v4 mode.

Install

pnpm add -D @cogs/kubb-transforms

Wiring

Add one command to your kubb.config.ts. pluginReactQuery must import from the react-query seam so the hook transforms recognise the output:

import { defineConfig } from 'kubb/config'

export default defineConfig({
  output: {
    path: './src',
    postGenerate: ['npx cogs-kubb-transforms run'],
  },
  plugins: [
    // ...pluginTs, pluginZod, pluginFetch
    pluginReactQuery({
      client: 'fetch',
      hooks: true,
      query: { methods: ['GET'], importPath: '@cogs/react-query' },
      mutation: {
        methods: ['POST', 'PUT', 'PATCH', 'DELETE'],
        importPath: '@cogs/react-query',
      },
    }),
  ],
})

The generated runtime (src/.kubb/client.ts) then imports @cogs/fetch-client/kubb-transport, so call configureClient({ baseUrl, ... }) from @cogs/fetch-client during app bootstrap.

The seam sets serializer.path through client.setConfig, which Kubb merges shallowly: a later client.setConfig({ serializer }) in app code replaces it and drops the dot-segment guard. Set the whole serializer object, or leave it alone. Changing the runtime.* options takes effect on the next kubb generate, which re-copies the runtime; re-running the CLI alone does not rewrite an existing seam.

Optionally drop a kubb-transforms.config.ts next to kubb.config.ts:

import { defineConfig } from '@cogs/kubb-transforms'

export default defineConfig({
  // Matches `output.path` in kubb.config.ts.
  outDir: './src',

  // Cross-package dedup map (see "Import map" below).
  map: './kubb-map.json',

  zod: {
    unwrapLazySafe: true,
  },
})

.json, .js, .mjs and .mts are also accepted. A TypeScript config is loaded through Node's native type stripping, which needs Node >= 22.18; on older runtimes use kubb-transforms.config.json.

CLI

npx cogs-kubb-transforms run [options]

  --config=<path>   Config file path. Default: probe kubb-transforms.config.*
                    in the current directory.
  --cwd=<path>      Directory to resolve config and output from.
  --dry-run         Report what would change without writing.
  --quiet           Suppress per-stage progress output.
  -h, --help        Show help.

Every transform is also importable as a pure (source, options) => string | null function, so you can drive them yourself:

import { resolveConfig, transformHooks } from '@cogs/kubb-transforms'

const config = resolveConfig({ hooks: { nextTags: false } })
const output = transformHooks(source, { config })

Stage auto-detection

A stage runs when its output exists: the hooks, types and zod stages when their directory does, the runtime stage when Kubb's bundled .kubb/client.ts does. You never declare "also run the hooks stage"; set hooks: false to opt out.

Stages run in order: zod → types → runtime → hooks.

Config

| Field | Default | Purpose | | --- | --- | --- | | root | cwd | Project root other paths resolve against | | outDir | ./src | Kubb's output.path | | dirs | {hooks, types, zod} | Per-generator subdirectory names | | map | — | Path to the cross-package import map JSON | | fetchClient | @cogs/fetch-client | Transport seam module specifier | | reactQuery | @cogs/react-query | Query seam module specifier | | quote | single | Re-print quote style | | semicolons | strip | Statement-terminator handling | | stripBanner | true | Remove the Generated by Kubb header (not from the runtime) | | runtime.file | .kubb/client.ts | Kubb's bundled runtime, relative to outDir | | runtime.transportModule | <fetchClient>/kubb-transport | Module exporting kubbTransport, rejectDotSegment, normalizeKubbError | | runtime.rejectDotSegments | true | Refuse a ./.. path param | | runtime.normalizeErrors | true | xior-style error message, .data mirrored onto .error | | hooks.mergeOptions | false | Skip injection when a caller spread is present | | hooks.placeholderData | true | Seed useQuery from cache | | hooks.nextTags | true | Merge options.next.tags = nextHashKeys(queryKey) into the client call | | hooks.mutationLifecycle | true | onMutate/onSuccess/onError/onSettled seams | | hooks.errorAlias | true | Factor the repeated error type into an alias | | hooks.transportErrors | true | Widen that alias to ResponseErrorConfig<...> \| XiorError: network and timeout failures reject with the raw xior error, not Kubb's ResponseError | | hooks.queryOptsParam | true | Third queryOpts param on *QueryOptions() | | hooks.queryKeyNote | false | Emit the explanatory queryKey NOTE comment | | hooks.queryOptionsSpreadName | resolvedOptions | Kubb's caller-options identifier | | hooks.mutationOptionsSpreadName | mutationOptions | Kubb's caller-options identifier | | zod.rewriteImports | true | Redirect duplicated schemas via the import map | | zod.unwrapLazySafe | false | Unwrap z.lazy() when provably acyclic | | types.rewriteImports | true | Redirect duplicated types via the import map |

Import map

When several generated API packages describe the same upstream model, each regenerates its own local copy. The map redirects the duplicates at one canonical package:

{
  "types": { "ApiError": "@acme/shared-types" },
  "zod":   { "apiErrorSchema": "@acme/shared-schemas" },
  "lazyUnwrap": { "deny": ["RecursiveSchema"] }
}

Passes: kept / dropped / adapted for Kubb v5

Every verdict was decided by generating real output with Kubb 5.12.6 (@kubb/plugin-fetch 5.7.1, @kubb/plugin-react-query 5.5.5, @kubb/plugin-ts 5.0.15, @kubb/plugin-zod 5.4.13) against a 4-endpoint scratch spec and reading it. The scratch spec and plugin config live in e2e/; src/__tests__/e2e.test.ts runs the real Kubb on every test run and fails if the captured fixtures in src/__tests__/fixtures.ts stop matching its output.

| Pass | Verdict | Evidence | | --- | --- | --- | | Fetch runtime → @cogs/fetch-client | NEW (replaces the client stage) | v5 no longer imports a client from @cogs/fetch-client/client; pluginFetch copies a ~1000-line runtime to .kubb/client.ts and every client, hook and handler sends through its exported client. One block is appended that calls client.setConfig({ transport }) with kubbTransport, wraps the path serializer with rejectDotSegment, and installs normalizeKubbError. Text-level and idempotent; throws if the runtime's layout changes. | | Client-stage passes (import merge, fetch → client, URL-helper collapse, requestData → data, confkey/operations.ts) | DROPPED | None of the shapes exist in v5: clients are withUnwrap(request({ method, url, ...config })), there are no *Url() helpers, no requestData, and pluginFetch has no operations option. The multi-API confKey registry routing has no v5 equivalent: kubbTransport sends through the single process-wide configureClient() instance, so one app talks to one configured API per process for now. | | Banner-comment strip | KEEP | Every generated file still opens with the Generated by Kubb banner (now *-prefixed). Skipped for the runtime, which Kubb re-copies verbatim. | | placeholderData + getPlaceholderData | KEEP | v5 hooks still destructure const { client: queryClient, ...resolvedOptions } = queryConfig and spread it into useQuery. | | Next.js cache tags | ADAPT | v5 has no config.signal anchor and the request type carries options?: FetchOptions (RequestInit & { next? }). Tags are now merged into the client call's options right after ...config (options: { ...config.options, next: { ...config.options?.next, tags } }) instead of mutating the caller's config. kubbTransport forwards options to fetch. | | queryOpts third parameter | KEEP | *QueryOptions() factories still build their options as an object literal passed to queryOptions<...>(). | | Mutation lifecycle skeleton | KEEP, bare pass-through | useMutation({ ...baseOptions, mutationKey, ...mutationOptions }) still has no lifecycle handlers. See the decision note below. | | addErrorAlias | ADAPT | v5 names no *QueryResponse types, so the alias is derived from the hook name (useGetWidget → GetWidgetErrorResponse, mutations get …MutationErrorResponse). An operation with no documented errors yields ResponseErrorConfig<Error>, which is aliased the same way. | | Zod .and() → .merge() | DROPPED | v5 emits allOf as base.extend({...}). | | Zod z.object({}).catchall(X) → z.record(X) | DROPPED | v5 emits additionalProperties as z.record(z.string(), X). zod.recordKeyType and zod.assumeImportedSchemas went with these two passes. | | Zod safe .lazy() unwrap | KEEP | v5 still wraps cross-file $refs in z.lazy(() => xSchema), including non-cyclic ones. Recursion inside a file is a getter (get child() { return widgetSchema.optional() }), so it never reaches this pass. A cycle spanning files cannot be proven from one file: deny those names in the import map, or leave zod.unwrapLazySafe off. | | Types-imports / zod-imports dedup | KEEP | Monorepo-structural and keyed by imported name, so independent of the Kubb version. |

Why the mutation skeleton stays a bare pass-through

The injected handlers are mutationOptions?.onSuccess?.(...) re-dispatches, not a ready-made optimistic update wired to @cogs/react-query's cancelAndSnapshot / rollback / bumpUpdatedAt.

An optimistic update has to know which query keys the mutation invalidates, and that is domain knowledge no OpenAPI document carries. A mutation's own generated mutationKey ([{ url: '/widgets' }]) is not the list or detail query key that needs cancelling and rolling back — deriving that requires cross-file inference the codegen cannot do. Emitting cancelAndSnapshot(queryClient, [/* ? */]) would produce code that either fails to compile or silently does nothing, on every regeneration.

The four seams are the honest deliverable: they are idempotent, semantically neutral, and they put the extension point exactly where the app author needs it. Wiring the determinism helpers into them is a per-mutation decision, and @cogs/react-query documents that pattern.

MCP output: out of scope

@kubb/plugin-mcp output is not transformed. It carries its own auth-token and tool-registration concerns, and no plugin set this package is built for emits it.

Regenerating the fixtures

pnpm --filter @cogs/kubb-transforms exec kubb generate --config e2e/kubb.config.ts

writes the raw output to e2e/.generated (gitignored). Copy the changed files into src/__tests__/fixtures.ts; e2e.test.ts tells you exactly which ones drifted. To move to a newer Kubb, bump the seven kubb / @kubb/* devDependencies together (they pin each other), run the tests, and fix whatever drifted.

License

MIT