TypeScript Bundle
$ 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 commandvovk 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:
- Generates a client into the
tmp_prebundlefolder (set bybundle.prebundleOutDir: string) from the tsBase template. - Calls the
bundle.buildfunction, which bundles the generated client into thedistfolder (set bybundle.outDir: string). - Generates
package.jsonandREADME.mdfrom the packageJson and readme templates, for the same segments and OpenAPI mixins as the bundled code. - Deletes the
tmp_prebundlefolder (bundle.keepPrebundleDir: booleankeeps it).
npx vovk bundleThen publish the package to NPM:
npm publish distConfiguring the bundle
Add a bundle object to the config file:
/** @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.
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:
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:
const config = {
// ...
bundle: {
outputConfig: {
imports: {
validateOnClient: null,
},
},
},
};Bundling with tsdown (Experimental)
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.0Add this build function to the bundle object in the config file:
/** @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:
/** @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-bundleThen 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
NextResponseoutputs can’t be inferred (see proposal ). For dynamic response headers, use theResponseclass instead, with a manual type cast. - ✨ Test and document other bundlers, such as tsup and esbuild.
- ✨ Segmented bundle: a separate bundle for each segment.