Skip to Content
vovk bundle 🧪

TypeScript Bundle (Experimental)

Quick CLI Ref
$ npx vovk bundle --help Usage: vovk bundle|b [options] Generate TypeScript Client 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-dir, --prebundle-out <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

The composed TypeScript Client library can be bundled and published to NPM as a zero-dependency package with pre-filled package.json and README.md files by using the bundle command after you configure the bundle.build function in the config file.

This feature is library-agnostic, so you can use any bundler you prefer, including one invoked via the child_process module. At the moment, tsdown  is the only bundler that has been tested with Vovk.ts. If you use a different bundler, please share your experience on GitHub Discussions .

Internally, bundling runs the following steps:

  1. It generates a client into the tmp_prebundle directory (configured with bundle.prebundleOutDir: string) using the tsBase template.
  2. Calls bundle.build function to bundle the generated client to the dist directory (configured with bundle.outDir: string).
  3. Generates package.json and README.md files from the packageJson and readme templates.
  4. Deletes the tmp_prebundle directory (configured with bundle.keepPrebundleDir: boolean).

After bundling, the package can be published to NPM:

npm publish dist

Configuring the bundle

You can configure bundling by adding a bundle object to the config file:

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

build function (required)

The build function is an asynchronous function that receives an object with entry (the index.ts file), outDir, and prebundleDir, all resolved as absolute paths.

prebundleOutDir or --prebundle-out flag

The prebundleOutDir is the directory in which the TypeScript client will be generated before bundling. It defaults to tmp_prebundle.

keepPrebundleDir or --keep-prebundle-dir flag

If set to true, the prebundleOutDir will not be deleted after bundling. This can be useful for debugging or other purposes. The default is false.

outputConfig

The outputConfig object accepts and overrides the same options as the outputConfig at the root of the config file.

For correct generation, the outputConfig should provide an origin option as well as a package field that includes entry point configuration: main, types, and exports, which should be set according to the bundler output.

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

You can also include additional exports in the generated index.ts file using the reExports option:

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

In order to keep the bundle size minimal, consider disabling client-side validation by setting validateOnClient to null in the imports option:

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

Bundling with tsdown

Install tsdown as a development dependency:

npm install --save-dev tsdown

Add the following 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'); await build({ entry, dts: true, format: 'esm', hash: false, fixedExtension: true, clean: true, outDir, platform: 'neutral', outExtensions: () => ({ js: '.js', dts: '.d.ts' }), outputOptions: { inlineDynamicImports: true, }, inputOptions: { resolve: { mainFields: ['module', 'main'], }, }, noExternal: ['!next/**'], }); }, // ... }, }; export default config;

With the configuration above, the resulting bundled package will have the following structure:

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

The full configuration with origin, package, and no client-side validation might look like this:

vovk.config.mjs
/** @type {import('vovk').VovkConfig} */ const config = { bundle: { build: async ({ entry, outDir }) => { const { build } = await import('tsdown'); await build({ entry, dts: true, format: 'esm', hash: false, fixedExtension: true, clean: true, outDir, platform: 'neutral', outExtensions: () => ({ js: '.js', dts: '.d.ts' }), outputOptions: { inlineDynamicImports: true, }, inputOptions: { resolve: { mainFields: ['module', 'main'], }, }, noExternal: ['!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, }, }, }, }; export default config;

For different tsdown configurations, please refer to the tsdown documentation .

Using the Bundled Package

After publishing the bundled package to NPM, you can install and use it in other projects like any other NPM package:

npm install my-api-bundle

And import it in your TypeScript code:

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

All features described in the TypeScript article remain available to the bundled RPC modules.

The Schema is available via the schema import, as well as a property on every method individually:

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

Note that the openapi object that is usually available from vovk-client/openapi is not bundled, as it would significantly increase the package size. The schema module that is usually available from vovk-client/schema is also omitted to keep the bundling flow simple by using only a single entry point.


The bundled methods can be used as AI tools that will invoke the corresponding HTTP endpoints when called:

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

Roadmap/bugs

  • 🐞 Type inference from NextResponse outputs is not available without the next package installed (see proposal ). For dynamic response headers, use the Response class instead, with manual type casting.
  • ✨ Segmented bundle — create separate bundles for each segment.
Last updated on