Creating VDL Plugins
Note: You do not need Node.js or any JavaScript runtime installed on your machine. VDL executes plugin JavaScript code through Goja, an embedded ECMAScript runtime built into the VDL binary.
The Short Version¶
A VDL plugin is just a JavaScript file that exports a generate(input) function.
exports.generate = (input) => {
return {
files: [
{
path: "summary.txt",
content: `This schema has ${input.ir.types.length} types.\n`,
},
],
};
};
Then reference it from vdl.config.vdl:
const config = {
version 1
plugins [
{
src "./plugins/summary.js"
schema "./schema.vdl"
outDir "./gen"
}
]
}
Run:
VDL compiles schema.vdl, calls your plugin function, and writes the returned files under ./gen.
How Plugins Work¶
The generation flow is simple.
- VDL reads
vdl.config.vdl. - VDL resolves each configured plugin source.
- VDL analyzes the configured
schemafile. - VDL converts the valid schema into the generator-facing IR.
- VDL executes the plugin's
generate(input)function in a JavaScript runtime. - The plugin returns generated files or structured errors.
- VDL validates output paths and writes files inside
outDir.
The plugin input has this shape:
The plugin output has this shape:
type PluginOutput = {
files?: Array<{
path: string;
content: string;
}>;
errors?: Array<{
message: string;
position?: {
file: string;
line: number;
column: number;
};
}>;
};
input.version is the VDL version. input.ir is the fully resolved Intermediate Representation of the schema. input.options contains the key-value options from the plugin block in vdl.config.vdl.
Options are strings in the config schema. If you need booleans, numbers, lists, or enums, parse and validate them in your plugin.
Minimal JavaScript Plugin¶
Create plugins/models-list.js:
exports.generate = (input) => {
const lines = [];
lines.push("# VDL Types");
lines.push("");
for (const typeDef of input.ir.types) {
lines.push(`- ${typeDef.name}`);
}
return {
files: [
{
path: "models.md",
content: `${lines.join("\n")}\n`,
},
],
};
};
Configure it:
const config = {
version 1
plugins [
{
src "./plugins/models-list.js"
schema "./schema.vdl"
outDir "./gen/docs"
}
]
}
Run:
Result:
Export Formats¶
VDL accepts any of these forms:
Important Runtime Rules¶
- The plugin runs as JavaScript, not as a full Node.js process.
- Keep the release artifact self-contained; do not rely on runtime filesystem or npm package loading.
console.log,console.info,console.warn, andconsole.errorare available for plugin logs.- Returned file paths must be relative and must stay inside
outDir. - If the plugin returns
errors, VDL stops and does not write generated files. - Throwing an unexpected error fails the plugin run.
- Remote plugins should be pinned to a release tag or full commit hash for reproducibility.
Plugin Sources¶
src in vdl.config.vdl can point to several kinds of plugin artifact.
| Source kind | Example | Notes |
|---|---|---|
Local .js file |
./plugins/my-plugin.js |
Must start with . or /. |
HTTPS .js URL |
https://example.com/vdl-plugin/index.js |
Must point to a .js file. |
| GitHub shorthand | varavelio/[email protected] |
Resolves to dist/index.js in that repo. |
GitHub shorthand repositories must be named with the vdl-plugin- prefix. VDL caches remote plugin files and records hashes in vdl.lock.
Authenticated Remote Plugins¶
Private plugin hosts can be configured through remotes in vdl.config.vdl. VDL matches each plugin URL against the most specific configured remote host and reads credentials from environment variables.
const config = {
version 1
remotes [
{
host "github.com/my-org/private-vdl-plugin"
auth {
github {
tokenEnv "GITHUB_TOKEN"
}
}
}
]
plugins [
{
src "my-org/[email protected]"
schema "./schema.vdl"
outDir "./gen/private"
}
]
}
Supported auth styles are GitHub token, custom header, bearer token, and basic auth.
Using The Plugin SDK¶
Plain JavaScript is enough for small plugins. For serious plugins, use @varavel/vdl-plugin-sdk.
The SDK gives you:
- typed
definePlugin(...)authoring API - typed access to the VDL IR and plugin input/output contracts
- structured error helpers such as
PluginError,fail, andassert - utility imports for strings, arrays, RPC validation, and other common generator tasks
- testing builders for realistic IR fixtures
- a
vdl-pluginCLI for type-checking and bundling plugins - TypeScript configuration presets for app and test code
Minimal SDK plugin:
import { definePlugin } from "@varavel/vdl-plugin-sdk";
export const generate = definePlugin((input) => {
return {
files: [
{
path: "hello.txt",
content: `Hello from VDL ${input.version}\n`,
},
],
};
});
How exports work: SDK plugins use
export const generate, notexports.generate. Thevdl-plugin buildcommand converts the ESM export intoexports.generateautomatically when bundling intodist/index.js. The final artifact that VDL loads always uses the CommonJSexports.generateform.
Check and build:
check runs TypeScript without emitting files. build bundles the plugin into dist/index.js, which is the artifact VDL consumes for GitHub shorthand plugins.
Starting From The Template¶
The fastest way to start a production-ready plugin is varavelio/vdl-plugin-template.
The template includes:
src/index.tswith a minimaldefinePlugin(...)entrypoint- the VDL plugin SDK
- TypeScript config
@varavel/genfor structured text/code generation- Biome and dprint for linting and formatting
- Vitest for tests
- a devcontainer
- a GitHub Actions CI workflow
dist/index.jsas the distributable plugin bundle
Basic workflow:
When releasing a GitHub-hosted plugin:
- Build with
npm run build. - Commit source files and
dist/index.js. - Create a release tag such as
v0.1.0. - Users can reference
owner/[email protected]fromvdl.config.vdl.
VDL plugins do not need to be published to npm. The GitHub release plus committed dist/index.js is enough for VDL to fetch the plugin.
Error Handling¶
Return structured errors when the user's schema or options are invalid.
import { definePlugin, fail } from "@varavel/vdl-plugin-sdk";
export const generate = definePlugin((input) => {
const root = input.options.root;
if (!root) {
fail('Missing required option "root".');
}
const typeDef = input.ir.types.find((item) => item.name === root);
if (!typeDef) {
fail(`Unknown root type "${root}".`);
}
return {
files: [{ path: "root.txt", content: `${typeDef.name}\n` }],
};
});
For schema-specific errors, attach a source position when possible. VDL will print the diagnostic with file, line, and column information.
Testing Plugins¶
The SDK testing entry point helps you build plugin inputs without manually writing a full IR object.
import {
field,
objectType,
pluginInput,
primitiveType,
schema,
typeDef,
} from "@varavel/vdl-plugin-sdk/testing";
const input = pluginInput({
options: { root: "User" },
ir: schema({
types: [
typeDef("User", objectType([field("id", primitiveType("string"))])),
],
}),
});
Pass the input to your plugin handler and assert generated files or returned errors.
Practical Checklist¶
- Validate plugin options before generating output.
- Validate annotation models before assuming a shape such as RPC or events.
- Generate deterministic output so diffs stay clean.
- Keep generated file paths relative to
outDir. - Return structured errors for user mistakes.
- Use the SDK for TypeScript, typed IR access, bundling, and tests.
- Commit
dist/index.jsfor GitHub-hosted plugins. - Pin plugin versions in consuming projects.