bundle — Claude Code skill: ship a Vovk.ts client as an npm package
Turns a generated Vovk.ts TypeScript client into a dependency-free npm package with JS, types, package.json and a README. Useful when the client has to leave the app and land in a registry.
npx skills add finom/vovk --skill bundle -a claude-codeWhat bundle does
The skill covers the vovk bundle command from Vovk.ts. Vovk first writes the client into prebundleOutDir (default tmp_prebundle), then hands control to your async bundle.build({ entry, outDir, prebundleDir }), where any bundler can be plugged in. After that Vovk emits package.json and a README with samples into outDir (default dist) and wipes the temporary folder unless keepPrebundleDir: true is set.
Only tsdown was tested by the authors, and they advise pinning [email protected] until newer releases are confirmed to work. Compilation runs in two passes: one for code, one for declarations only. origin is required in outputConfig — it is the API base URL baked into the client. Setting imports.validateOnClient: null drops AJV checks on the client side and cuts the package size noticeably. Note that includeSegments and excludeSegments sit on the root bundle config, not inside outputConfig.
This is TypeScript only. Python and Rust clients have their own templates and separate skills. Running vovk bundle during everyday development is a waste — vovk generate is faster and keeps the client inside the project. Reach for the bundle when you actually need a registry artifact.
What it helps with
- package a Vovk.ts client as a zero-dependency npm module
- wire tsdown into the client build without losing type declarations
- shrink the published package by disabling client-side AJV validation
- add custom exports and a README banner to the generated package
- filter out API segments that should not ship in the SDK
Example prompts for the agent
- Bundle the Vovk client into a publishable package and show me what lands in
dist - Set up
bundle.buildwith[email protected]and the two-pass build - Turn off
imports.validateOnClientand tell me how much smaller the bundle gets
How to install bundle
1Skills CLI (Vercel)
npx skills add finom/vovk --skill bundle -a claude-code2Manually: put the files in place
~/.claude/skills/bundle/SKILL.mdCopy the whole skill folder; for one project use .claude/skills/bundle/
1Skills CLI (Vercel)
npx skills add finom/vovk --skill bundle -a codex2Manually: put the files in place
.agents/skills/bundle/SKILL.mdFor all projects use ~/.agents/skills/
1Skills CLI (Vercel)
npx skills add finom/vovk --skill bundle -a cursor2Manually: put the files in place
.cursor/skills/bundle/SKILL.mdCursor also reads .agents/skills/ and .claude/skills/
1GitHub CLI
gh skill install finom/vovk bundle2Manually: put the files in place
.github/skills/bundle/SKILL.md1Skills CLI (Vercel)
npx skills add finom/vovk --skill bundle -a gemini-cli2Platform CLI
gemini skills install https://github.com/finom/vovk --path skills/bundleManually: put the files in place
.devin/skills/bundle/SKILL.mdThe legacy .windsurf/skills/ path still works
Skills CLI (Vercel)
npx skills add finom/vovk --skill bundle -a opencodeClaude.ai
Zip the folder and upload it in Settings → Capabilities → Skills
Step by step, with commands for each platform: How to install skills in Claude Code: five ways
Questions about bundle
How do I install the bundle skill in Claude Code?
Run npx skills add finom/vovk --skill bundle -a claude-code. The skill file ends up at ~/.claude/skills/bundle/SKILL.md.
What does the bundle skill do?
It documents packaging a generated Vovk.ts TypeScript client into an npm package: prebundle step, the bundle.build hook, generated package.json and README, and a dist folder ready for npm publish.
How is vovk bundle different from vovk generate?
vovk generate writes the client and OpenAPI spec into the project and runs on every build. vovk bundle produces a standalone package meant for the registry.
Source text
SKILL.md · MIT
--- name: bundle description: "Vovk.ts `vovk bundle` CLI — packages composed TypeScript client as zero-dep publishable npm package. Covers `bundle.build` async fn, `[email protected]` recipe, `outputConfig.origin` / `package` / `reExports` / `imports.validateOnClient: null`, `prebundleOutDir` / `outDir` / `keepPrebundleDir`, `--include`/`--exclude` segments, `--openapi-*` mixin flags. Use when user asks to \"publish my API client\", \"ship an SDK to npm\", \"build a publishable client package\", \"bundle the Vovk client\", \"generate a distributable npm SDK\", \"use tsdown with vovk\", or variations including why bundled package omits `openapi`/`schema` entry points. Note: `vovk bundle` is **TypeScript-only**. For Python/PyPI publishing, hand off to **`python`** skill. For Rust/crates.io publishing, hand off to **`rust`** skill. Does NOT cover in-project client codegen (`@/client`) → **`rpc`** skill." --- # Vovk.ts `vovk bundle` `vovk bundle` packages composed TypeScript client as zero-dep npm package — pre-built JS + `.d.ts`, auto-gen `package.json`, auto-gen `README.md` w/ samples, ready for `npm publish`. Release step: `vovk generate` → in-project client for dev; `vovk bundle` → distributable SDK installed from registry. **TypeScript-only.** Despite name, ships no Python/Rust clients. Those have own templates (`py` / `pySrc` / `rs` / `rsSrc`) and own publish flows — see **`python`** and **`rust`** skills. ## `bundle` vs `generate` | Command | Purpose | Output | When | |---|---|---|---| | `vovk generate` | In-project TypeScript client + OpenAPI spec | `src/client/` (or `client/` without `src/app`), `.vovk-schema/` | Every build (`prebuild` hook), on schema changes during `vovk dev`. | | `vovk bundle` | Publishable npm package — pre-built JS + `.d.ts` + `package.json` + `README.md` | `dist/` ready for `npm publish` | Ship SDK outside Next.js app. | Don't run `vovk bundle` in normal dev; in-project client faster. Bundle when you need registry artifact. ## How bundling works 1. Vovk gens TypeScript client into `prebundleOutDir` (default `tmp_prebundle`) via `tsBase` template — uncompiled `.ts` source. 2. Vovk calls your `bundle.build({ entry, outDir, prebundleDir })` async fn. Plugs in any bundler (tsdown only tested one) to compile `entry` (the `tmp_prebundle/index.ts`) → JS + `.d.ts` under `outDir` (default `dist`). 3. Vovk emits `package.json` and `README.md` from `packageJson` and `readme` templates into `outDir`. 4. Vovk deletes `prebundleOutDir` unless `keepPrebundleDir: true` (handy for debugging prebundled source). ```sh npx vovk bundle npm publish dist ``` Published package consumed exactly like the in-project `@/client`: ```ts import { UserRPC } from 'my-api-bundle'; await UserRPC.getUser({ params: { id: '123' } }); ``` All TS-client features (call shape, `withDefaults`, type inference, `schema` access) survive bundle. ## `bundle.build` — required entry point `bundle.build` is async fn. Vovk calls w/ absolute paths: ```ts build: async ({ entry, outDir, prebundleDir }) => { // entry — absolute path to the prebundled tmp_prebundle/index.ts // outDir — absolute path where the bundler should emit JS + .d.ts // prebundleDir — absolute path of the prebundle dir (rarely needed) } ``` Vovk is bundler-agnostic — `esbuild`, `tsup`, `tsdown`, even `child_process` shell-out work. Only **tsdown** tested by Vovk's authors; docs warn: **pin `[email protected]`** until newer versions confirmed compatible. ### Canonical `tsdown` recipe (pin 0.23.0) ```sh npm install --save-dev [email protected] ``` ```ts filename="vovk.config.mjs" /** @type {import('vovk').VovkConfig} */ const config = { bundle: { build: async ({ entry, outDir }) => { const { build } = await import('tsdown'); // the code and the types build in two passes: one pass writes an extra chunk.js for (const dts of [false, { emitDtsOnly: true }]) { await build({ entry, dts, format: 'esm', hash: false, fixedExtension: true, clean: !dts, outDir, platform: 'neutral', outExtensions: () => ({ js: '.js', dts: '.d.ts' }), outputOptions: { codeSplitting: false }, inputOptions: { resolve: { mainFields: ['module', 'main'] } }, deps: { alwaysBundle: ['!next/**'] }, }); } }, outputConfig: { origin: 'https://example.com', package: { main: './index.js', types: './index.d.ts', exports: { '.': { default: './index.js', types: './index.d.ts' } }, }, imports: { validateOnClient: null }, // shrinks the bundle considerably }, }, }; export default config; ``` Resulting `dist/` layout: ``` dist/ ├── package.json ├── README.md ├── index.js └── index.d.ts ``` ## `bundle.outputConfig` — what goes in package `bundle.outputConfig` accepts same options as root-level `outputConfig` (bundle-specific overrides win). Key fields: | Field | Purpose | |---|---| | `origin` *(required)* | Default API base URL baked into bundled client. Override at call time via `apiRoot`. | | `package` | Overrides for generated `package.json`. Set `name`, `version`, `type`, `main`, `types`, `exports` here. Defaults pull from project root `package.json` where present. | | `imports.validateOnClient: null` | Disables client-side AJV validation in bundle — usually big size win. | | `imports.fetcher` | Custom fetcher path baked into bundle. | | `reExports` | Extra named exports in generated `index.ts`. E.g. `{ doSomething: './src/utils' }` exposes helper alongside RPC modules. | | `readme.banner` | String prepended to generated `README.md` (badges, logo HTML, status callouts). | | `readme.installCommand` | Override install command in README (default: `npm install <package-name>`). For monorepo / private-registry / `pnpm` / `yarn`-first audiences. | | `readme.description` | Replaces description block in README. Defaults to bundled `package.json` description. | | `samples.apiRoot` | Base URL in generated README samples. Defaults to `outputConfig.origin`. Set to public-facing URL when `origin` points to staging/internal hostname. | | `samples.headers` | Header map (e.g. `{ Authorization: 'Bearer YOUR_TOKEN' }`) in generated samples — shows consumers how to auth. | Min viable `outputConfig` is `origin` + `package` block with `main`/`types`/`exports` matching bundler's output filenames. ### Root `bundle` keys (NOT under `outputConfig`) These three live on root `VovkBundleConfig` (`config.bundle.*`), **not** nested under `outputConfig`: | Field | Purpose | |---|---| | `requires` | Template dir overrides — `readme`, `packageJson`, or any custom template. Defaults to project root for both. | | `includeSegments` / `excludeSegments` | Filter which Vovk segments contribute to bundle. Read from the root `bundle` config. | ## CLI flags ``` $ npx vovk bundle --help Usage: vovk bundle|b [options] --out, --out-dir <path> path to output directory for bundle --prebundle-out-dir, --prebundle-out <path> path to output directory for prebundle --keep-prebundle-dir do not delete prebundle directory after bundling --include, --include-segments <segments...> include segments --exclude, --exclude-segments <segments...> exclude segments --schema, --schema-path <path> path to schema folder (default: .vovk-schema) --config, --config-path <config> path to config file --origin <url> set the origin URL for the generated client --openapi, --openapi-spec <paths_or_urls...> use OpenAPI mixins for client generation --openapi-module-name <names...> module name strategies, indexed against --openapi --openapi-method-name <names...> method name strategies, indexed against --openapi --openapi-root-url <urls...> root URLs, indexed against --openapi --openapi-mixin-name <names...> mixin names, indexed against --openapi --openapi-fallback <paths...> fallback files for --openapi URLs --log-level <level> set the log level ``` `--openapi-*` family is positional — one entry per spec to `--openapi`, matching entry at same index to each subsidiary flag. Lets you bundle client combining your own segments with OpenAPI mixins w/o touching config file (handy for one-off CI bundles). ## What's bundled, what's omitted **Included:** all RPC modules (with `@operation`-driven types), `schema` named export, type inference helpers, `withDefaults` method on each module. **Omitted:** - **`openapi` object** (would inflate bundle size). Consumers needing OpenAPI spec should hit runtime endpoint serving it (see **`openapi`** skill). - Separate **`schema` module entry point** — bundle is single-entry by design. `schema` named export still importable from package root, `<RpcModule>.<method>.schema` still on every method: ```ts import { schema, UserRPC } from 'my-api-bundle'; console.log(schema.segments[''].controllers.UserRPC.handlers.getUser.validation.params); console.log(UserRPC.getUser.schema.validation.params); ``` ## Bundled SDK + `deriveTools` Bundled modules feed `deriveTools` identically to native ones — same call sig, same `@operation` metadata, same tool surface: ```ts import { UserRPC } from 'my-api-bundle'; import { deriveTools } from 'vovk'; const tools = deriveTools({ modules: { UserRPC } }); ``` See **`tools`** skill for full pipeline. ## Flows ### "Publish a TypeScript SDK for my Vovk API" 1. Add `bundle` to `vovk.config.mjs` w/ `build` fn (use tsdown recipe above) and `outputConfig` containing `origin` + `package` block. 2. `npx vovk bundle`. 3. `npm publish dist`. ### "Bundle a subset of segments as a public SDK" Set `bundle.includeSegments` (or pass `--include` on CLI) to ship only public-facing segments. Under `bundle.outputConfig` the key is ignored and every segment ships. Useful when `admin`/`internal` segments live in same Vovk app but only `public` ships to consumers. ### "Bundle our API combined with a third-party OpenAPI spec" Either configure mixins in `vovk.config.mjs` (see **`mixins`** skill) and run `npx vovk bundle`, or use CLI's `--openapi*` flags for one-off: ```sh npx vovk bundle \ --openapi https://petstore3.swagger.io/api/v3/openapi.json \ --openapi-module-name PetstoreAPI \ --openapi-method-name auto \ --openapi-root-url https://petstore3.swagger.io/api/v3 ``` ### "My bundle is too large" Disable client-side validation: `bundle.outputConfig.imports.validateOnClient: null`. Most apps consuming typed SDK trust their own input; AJV adds runtime weight often unneeded post-publish. ### "Debug the prebundled TypeScript before bundling" Set `bundle.keepPrebundleDir: true` (or pass `--keep-prebundle-dir`). `tmp_prebundle` survives bundle run so you can inspect generated `index.ts` and module sources. ## Gotchas - **`bundle` is TypeScript-only.** Don't reach for Python/Rust SDKs — those use `vovk generate` with `py`/`pySrc`/`rs`/`rsSrc` templates + language-native publish commands. See **`python`** / **`rust`** skills. - **Pin `[email protected]`.** Docs warn tsdown's API may break between minor versions; recipe above validated against 0.23.0. No floating range until upstream stability confirmed. - **`origin` required.** Without it, bundled client has no default API base URL and consumers must pass `apiRoot` on every call. Bake it in — or use `withDefaults` post-import as one-off. - **`NextResponse` type inference needs `next` package** at consumer install time (Next.js limitation, not Vovk's). For dynamic response headers in bundled controller, return plain `Response` w/ manual type casting. - **`openapi` module isn't bundled** — consumers needing spec should fetch from runtime endpoint (see **`openapi`** skill's `OpenApiController` pattern). Deliberate size choice. - **No segmented bundle yet** — bundled package is single-entry. Segmented bundling (one package per segment) is roadmap item; for now, run `vovk bundle` once per segment-subset via `includeSegments` if you need separate packages. - **Don't bake secrets.** API tokens and signing keys must not ship in published package. Document consumers configure auth themselves (via `withDefaults` or custom fetcher). - **CI placement.** Bundle in release workflow triggered by tag or manual dispatch, not every PR. Build step heavier than `vovk generate` and produces artifact you don't want piling up in main-branch CI. - **`package.json` defaults inherit from project root.** Set `bundle.outputConfig.package.name` (and `version`) explicitly when project root's name isn't a good public package name — almost always the case for Next.js apps named after product, not SDK.
© finom · MIT · finom/vovk