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.2.0

Published

jscodeshift-based code transforms for migrating and maintaining Kubb-generated codegen output

Readme

@cogs/kubb-transforms

Post-generation codemods for Kubb output.

Kubb generates a correct but deliberately minimal data layer. This package closes the gap between "compiles" and "production-ready" — merging the split transport imports, collapsing the URL helpers, wiring confkey routing into @cogs/fetch-client's operation registry, seeding placeholderData, adding Next.js cache tags via @cogs/react-query's nextHashKeys, opening the four mutation lifecycle seams Kubb emits none of, and converging zod intersections into merges.

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

Install

pnpm add -D @cogs/kubb-transforms

Wiring

Add one line to your kubb.config.ts:

export default defineConfig({
  // ...plugins
  hooks: {
    done: ['npx cogs-kubb-transforms run'],
  },
})

Then drop a kubb-transforms.config.ts next to it:

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

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

  // Routes generated requests to a named entry in @cogs/fetch-client's
  // config registry: setConfig('envmgr', { baseUrl, getToken }).
  clients: {
    confKey: 'envmgr',
    registerName: 'registerEnvmgrOperations',
  },

  // 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, transformClient } from '@cogs/kubb-transforms'

const config = resolveConfig({ clients: { confKey: 'envmgr' } })
const output = transformClient(source, { config, operationId: 'getWidget' })

Stage auto-detection

A stage runs when its output directory exists and contains .ts files — mirroring the shopt -s nullglob guards in the shell scripts this replaces. You never declare "also run the hooks stage"; set hooks: false to opt out.

Stages run in dependency order: zodtypesclientshooks.

Config

| Field | Default | Purpose | | --- | --- | --- | | root | cwd | Project root other paths resolve against | | outDir | ./src | Kubb's output.path | | dirs | {clients, 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 | | clients.confKey | — | Injected as confkey into requests + operations.ts | | clients.registerName | registerOperations | Name of the appended registration function | | clients.operationsFile | operations.ts | Basename of the generated registry | | clients.collapseUrlHelpers | true | {method,url} object → bare string return | | clients.renameRequestData | true | Drop the requestData hop | | clients.mergeFetchClientImports | true | Merge the two seam imports into one | | hooks.mergeOptions | false | Skip injection when a caller spread is present | | hooks.placeholderData | true | Seed useQuery from cache | | hooks.nextTags | true | config.next = { tags: nextHashKeys(queryKey) } | | hooks.mutationLifecycle | true | onMutate/onSuccess/onError/onSettled seams | | hooks.errorAlias | true | Factor the repeated error type into an alias | | 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.mergeIntersections | true | .and().merge() when provably safe | | zod.catchallToRecord | true | z.object({}).catchall(X)z.record(X) | | zod.recordKeyType | — | Set to 'z.string()' on Zod 4 (two-arg z.record) | | zod.unwrapLazySafe | false | Unwrap z.lazy() when provably acyclic | | zod.assumeImportedSchemas | true | Trust imported *Schema names as objects |

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"] }
}

Ported passes: keep / drop / adapt

All five original scripts came from envmgr-ui/scripts/transform-kubb-*.js, written against an older Kubb and against @ad-infrastructure/fetch-client (a xior wrapper). Every verdict below was decided by generating real output with Kubb v4.39.3 against a 4-endpoint scratch spec (GET list, GET by id, POST with body, PATCH with body) and reading it — not by assumption. The captured "before" shapes live in src/__tests__/fixtures.ts.

| Pass | Verdict | Evidence | | --- | --- | --- | | Banner-comment strip | KEEP (+ fix) | Every generated file still opens with /** Generated by Kubb ... Do not edit manually. */. Fixed a latent bug: the strip used to be folded into other passes, so a file nothing else touched kept its banner. It is now an independent pass. | | Fetch-client import merge/rewrite | ADAPT | v4 emits two imports from the same specifier — import fetch from '@cogs/fetch-client/client' and import type { Client, RequestConfig, ResponseErrorConfig } from '@cogs/fetch-client/client'. The original split them across the package root and the /client subpath. Both now merge into one statement with per-specifier type modifiers. | | fetchclient rename | KEEP in clients, DROP in hooks | Client files really do bind fetch (const { client: request = fetch } = config), shadowing the global. Hook files import the client function by name (import { listWidgets } from '../../clients/listWidgets.ts') and never bind fetch — the pass was a no-op there, and the original additionally injected an unreferenced import type client from '<seam>/client'. | | XiorError<T> wrap of ResponseErrorConfig<T> | DROP | Raw output emits ResponseErrorConfig<GetWidget404> bare. @cogs/fetch-client's FetchClientError<T> implements ResponseErrorConfig<T> directly, so the generated type is already correct. No analogous rename or wrap is needed — verified by reading the raw output, not inferred from the type definition. | | URL-helper collapse | KEEP | v4 emits function getGetWidgetUrl(...) { const res = { method: 'GET', url: \/widgets/${id}` as const }; return res }— unexported — and calls it asgetGetWidgetUrl({...}).url.toString(). Collapsed to a bare string return, exported, and the call site shortened to .toString(). | | requestDatadata | **KEEP** | Present verbatim in POST and PATCH clients (const requestData = data; ... data: requestData); absent in GET. Shorthand is restored after the rename. | | confkey/operationIdinjection + registry | **KEEP, rewired** |operations.tsis emitted untyped with onlypath/method. Now annotated Operationsand populated withconfkey+operationIdfrom@cogs/fetch-client's real API, replacing the ad-hoc Operationstype the original invented. Registration is an exported **named function** rather than a module-scopeaddOperations(operations)— importing a generated barrel should not silently mutate a process-wide registry, andaddOperationsthrows on conflicting keys, which would turn an innocent import into a crash. | |addErrorAlias| **KEEP** (+ fix) | The identical error type repeats 3× in a query hook and 5× in a mutation hook. Two fixes: it now matches **balanced angle brackets** instead of a non-greedy/ResponseErrorConfig<(.*?)>/, which truncated nested generics like ResponseErrorConfig<Foo>; and it requires 2+ occurrences, since aliasing a single use is noise. | | placeholderData+getPlaceholderData| **KEEP, adapted** | RawuseQueryhas noplaceholderData. The caller-options spread is ...resolvedOptions, **not** ...queryOptionsas the original assumed —const { client: queryClient, ...resolvedOptions } = queryConfig. Injection now happens *after* the spread rather than before it, and the identifier is configurable. | | config.next = { tags: nextHashKeys(queryKey) }| **KEEP, adapted** | Raw query fns have noconfig.next. Wired to @cogs/react-query's real nextHashKeysexport. The original anchored the insertion after aconfig.signal = signalstatement; v4 has none (it passes{ ...config, signal: config.signal ?? signal }inline), so the assignment now goes to the top of the query fn — still before the spread readsconfig. The signal anchor is retained as a fallback for older output. | | Mutation lifecycle skeleton | **KEEP, bare pass-through** | Raw useMutation({ ...baseOptions, mutationKey, ...mutationOptions })has no lifecycle handlers. See the decision note below. | | Zod.and().merge()| **KEEP** (+ heuristic fix) | Confirmed:allOfbecomesz.lazy(() => a).and(z.lazy(() => b)).and(z.object({...})). One adaptation — assumeImportedSchemasonly trusted specifiers containing/zod/, but v4 emits sibling relative imports (./widgetBaseSchema.ts), so the convergence never fired. Relative specifiers now count. | | Zod z.object({}).catchall(X)z.record(X)| **KEEP** | Confirmed:additionalProperties: {type: string}becomesz.object({}).catchall(z.string()). Added zod.recordKeyTypebecause Zod 4 requires a two-argumentz.record. | | Zod safe .lazy()unwrap | **KEEP** | Confirmed and load-bearing: the scratch spec's self-referentialWidget.child?: Widgetproduces a genuinely recursive schema. Tarjan SCC cycle detection keeps that one wrapped while unwrapping the safe operation-level lazies. | | Types-imports / zod-imports dedup | **KEEP** | Monorepo-structural, independent of the transport. v4's new explicit.ts` extensions in specifiers do not affect it — the map is keyed by imported name, not module specifier. |

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 transforms: out of scope

transform-kubb-mcp.js and transform-kubb-mcp-tools.js (~980 lines) are not ported. They target @kubb/plugin-mcp output, which none of the kubb.config.ts plugin sets this package is built for emits, and they carry their own auth-token and tool-registration concerns. Keeping them out holds the initial surface to the five transforms every consumer actually runs; they can be added later as a mcp stage without disturbing anything here.

License

MIT