Skip to Content
vovk bundle

TypeScript Bundle

Quick CLI Ref
$ npx vovk bundle --help Usage: vovk bundle|b [options] Generate TypeScript RPC and bundle it Options: --out, --out-dir <path> path to output directory for bundle --include, --include-segments <segments...> include segments --exclude, --exclude-segments <segments...> exclude segments --prebundle-out, --prebundle-out-dir <path> path to output directory for prebundle --keep-prebundle-dir do not delete prebundle directory after bundling --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 <openapi_path_or_urls...> use OpenAPI mixins for client generation --openapi-module-name, --openapi-get-module-name <names...> module name strategies corresponding to the index of --openapi option --openapi-method-name, --openapi-get-method-name <names...> method name strategies corresponding to the index of --openapi option --openapi-root-url <urls...> root URLs corresponding to the index of --openapi option --openapi-mixin-name <names...> mixin names corresponding to the index of --openapi option --openapi-fallback <paths...> save OpenAPI spec corresponding to the index of --openapi option to a local file and use it as a fallback if URL is not available --log-level <level> set the log level -h, --help display help for command

vovk bundle turns the composed TypeScript client into a package for NPM, with its package.json and README.md filled in. Set the bundle.build function in the config file first.

The “Hello World” page has a full bundling example.

Any bundler works, even one you start with the child_process module. So far, tsdown  is the only bundler tested with Vovk.ts. If you use another one, share how it went on GitHub Discussions .

Bundling runs these steps:

  1. Generates a client into the tmp_prebundle folder (set by bundle.prebundleOutDir: string) from the tsBase template.
  2. Calls the bundle.build function, which bundles the generated client into the dist folder (set by bundle.outDir: string).
  3. Generates package.json and README.md from the packageJson and readme templates, for the same segments and OpenAPI mixins as the bundled code.
  4. Deletes the tmp_prebundle folder (bundle.keepPrebundleDir: boolean keeps it).
npx vovk bundle

Then publish the package to NPM:

npm publish dist

Configuring the bundle

Add a bundle object to the config file:

vovk.config.mjs
/** @type {import('vovk').VovkConfig} */ const config = { bundle: { build: async ({ entry, outDir, prebundleDir }) => { // call your bundler here }, prebundleOutDir: 'tmp_prebundle', // default keepPrebundleDir: false, // default outDir: 'dist', // default requires: { readme: '.', // default packageJson: '.', // default myTemplate: './foo', // custom template }, excludeSegments: ['admin'], // or includeSegments, not both outputConfig: { origin: 'https://example.com', package: { // the package.json content // by default, the values of the root package.json name: 'my-api-bundle', // the entry points type: 'module', main: './index.js', types: './index.d.ts', exports: { '.': { types: './index.d.ts', default: './index.js', }, }, }, readme: {}, // the README.md content samples: {}, // the code samples in README.md imports: { fetcher: './src/my-fetcher', }, reExports: {}, // re-exports in the generated index.ts }, }, }; export default config;

build function (required)

build is an async function. It gets an object with entry (the index.ts file), outDir and prebundleDir, all absolute paths.

prebundleOutDir or --prebundle-out flag

The folder the TypeScript client is generated in before bundling. Defaults to tmp_prebundle.

It must be a subfolder of the project, apart from outDir. Unless it’s kept, it’s deleted after bundling, so it must be missing, empty or kept by an earlier bundle: if it holds other files, the command fails before it writes anything.

keepPrebundleDir or --keep-prebundle-dir flag

true keeps prebundleOutDir after bundling, so it can hold other files, such as the composed client. Use it for debugging, for example. Defaults to false.

requires

The templates rendered into outDir after the build: template names mapped to paths relative to outDir. Defaults to { readme: '.', packageJson: '.' }. A requires object in the config replaces the default, so to keep readme and packageJson, list them with your own templates.

includeSegments and excludeSegments or --include and --exclude flags

The segments to bundle. Use one of the two. Without either, the bundle uses the includeSegments or excludeSegments of the composed client.

outputConfig

Takes the same options as the outputConfig at the root of the config file, and overrides them.

Set origin, and a package field whose entry points (main, types and exports) match the bundler output.

vovk.config.mjs
const config = { // ... bundle: { outputConfig: { origin: 'https://example.com', package: { main: './index.js', types: './index.d.ts', exports: { '.': { types: './index.d.ts', default: './index.js', }, }, }, }, }, };

To export more from the generated index.ts, use the reExports option:

vovk.config.mjs
const config = { // ... bundle: { outputConfig: { reExports: { 'doSomething': './src/utils', }, }, }, };

To keep the bundle small, turn off client-side validation: set validateOnClient to null in the imports option:

vovk.config.mjs
const config = { // ... bundle: { outputConfig: { imports: { validateOnClient: null, }, }, }, };

Bundling with tsdown (Experimental)

Important

The tsdown API can break between minor versions. The config below is tested with tsdown@0.23.0. If a newer version breaks it, pin this one until newer versions are confirmed to work.

Install tsdown as a dev dependency:

npm install --save-dev tsdown@0.23.0

Add this build function to the bundle object in the config file:

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/**'] }, }); } }, // ... }, }; export default config;

With this config, the bundled package looks like this:

    • package.json
    • README.md
    • index.js
    • index.d.ts

A full config with origin, package and no client-side validation:

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: { '.': { types: './index.d.ts', default: './index.js', }, }, }, imports: { validateOnClient: null, }, }, }, }; export default config;

For other tsdown options, see the tsdown documentation .

Using the Bundled Package

Once the package is on NPM, install it in another project like any other package:

npm install my-api-bundle

Then import it in your TypeScript code:

import { UserRPC } from 'my-api-bundle'; await UserRPC.getUser({ params: { id: '123' }, });

The bundled RPC modules have all the features of the TypeScript client.

The schema is the schema export, and each method has its own schema property:

import { schema, UserRPC } from 'my-api-bundle'; console.log(schema.segments[''].controllers.UserRPC.handlers.getUser.validation.params); console.log(UserRPC.getUser.schema.validation?.params);

The bundle leaves out the openapi object of @/client/openapi, as it would make the package much larger. It also has no separate schema entry point like @/client/schema: to keep bundling simple, the bundle has one entry point.


Pass the bundled RPC modules to deriveTools for AI tools that call the matching HTTP endpoints:

import { UserRPC } from 'my-api-bundle'; import { deriveTools } from 'vovk'; const tools = deriveTools({ modules: { UserRPC }, });

Roadmap/bugs

  • 🐞 Without the next package installed, the types of NextResponse outputs can’t be inferred (see proposal ). For dynamic response headers, use the Response class instead, with a manual type cast.
  • ✨ Test and document other bundlers, such as tsup and esbuild.
  • ✨ Segmented bundle: a separate bundle for each segment.
Last updated on