Writing Plugins
Plugins are how you extend what jiki can do — resolve custom imports, stub out modules, transform code, or add shell commands. Below are some common patterns, each as a standalone plugin you can adapt to your needs.
Virtual modules
The most common plugin pattern: resolve a custom specifier to a virtual path, then provide its contents via onLoad. No file needs to exist on the virtual filesystem.
import { boot, type JikiPlugin } from "@run0/jiki";
const configPlugin: JikiPlugin = {
name: "virtual-config",
setup(hooks) {
hooks.onResolve(/^virtual:config$/, () => ({
path: "/__virtual__/config.js",
}));
hooks.onLoad(/^\/__virtual__\/config\.js$/, () => ({
contents: `module.exports = ${JSON.stringify({
apiUrl: "https://api.example.com",
debug: true,
})};`,
}));
},
};
const container = boot({ plugins: [configPlugin] });
const result = container.execute('module.exports = require("virtual:config");');
console.log(result.exports); // { apiUrl: "https://api.example.com", debug: true }
CSS module stub
When running Node-style code that imports .css files, you can stub them out so require() doesn’t throw:
const cssStubPlugin: JikiPlugin = {
name: "css-stub",
setup(hooks) {
hooks.onLoad(/\.css$/, () => ({
contents: "module.exports = {};",
}));
},
};
For CSS modules, return a proxy that generates class names:
const cssModulesPlugin: JikiPlugin = {
name: "css-modules",
setup(hooks) {
hooks.onLoad(/\.module\.css$/, () => ({
contents: `module.exports = new Proxy({}, {
get: (_, key) => "css_" + key
});`,
}));
},
};
Code instrumentation
Use onTransform to inject code before every file executes. Transforms run as a pipeline, so multiple plugins can each add their own instrumentation.
const timingPlugin: JikiPlugin = {
name: "timing",
setup(hooks) {
hooks.onTransform(/\.js$/, args => ({
contents: `const __start = Date.now();\n${args.contents}\nconsole.log("${args.path}:", Date.now() - __start, "ms");`,
}));
},
};
Custom shell commands
Register commands that users can invoke via container.run():
const toolsPlugin: JikiPlugin = {
name: "tools",
setup(hooks) {
// "now" command prints the current timestamp
hooks.onCommand("now", () => ({
stdout: new Date().toISOString() + "\n",
stderr: "",
exitCode: 0,
}));
// "count" command counts files in a directory
hooks.onCommand("count", (args, ctx) => {
const dir = args[0] || ctx.cwd;
try {
const entries = ctx.vfs.readdirSync(dir);
return {
stdout: `${entries.length} entries\n`,
stderr: "",
exitCode: 0,
};
} catch {
return {
stdout: "",
stderr: `count: ${dir}: not found\n`,
exitCode: 1,
};
}
});
},
};
const container = boot({ plugins: [toolsPlugin] });
await container.run("now"); // prints ISO timestamp
await container.run("count /"); // prints entry count
Lifecycle hooks
React to container events:
const lifecyclePlugin: JikiPlugin = {
name: "lifecycle",
setup(hooks) {
hooks.onBoot(() => {
console.log("Container is ready");
});
hooks.onInstall(packages => {
console.log("Installed:", packages.join(", "));
});
},
};
Composing multiple plugins
Plugins are independent and compose naturally. Pass them as an array — hooks execute in the order plugins are listed:
const container = boot({
plugins: [
configPlugin, // virtual modules
cssStubPlugin, // CSS handling
timingPlugin, // instrumentation
toolsPlugin, // shell commands
lifecyclePlugin, // logging
],
});
For onResolve and onLoad, the first plugin to return a result wins. For onTransform, all matching plugins run in order. This means you can layer transforms from different plugins without conflicts.
Registering plugins after boot
If you need to add a plugin to an existing container:
import { boot, registerPlugin } from "@run0/jiki";
const container = boot();
// ... later ...
registerPlugin(container, myPlugin);
This registers all hooks and makes any new shell commands available immediately.
Tips
- Specific filters tend to perform better. A broad filter like
/./runs on every module and can slow things down — something like/^virtual:/or/\.custom$/is usually a better choice. - For virtual modules,
onResolveandonLoadwork as a pair:onResolvemaps the specifier to a unique path, andonLoadprovides the contents for that path. - Transforms are cumulative — each one in the pipeline receives the output of the previous one, so ordering matters when multiple plugins transform the same files.
- Plugins are isolated per container. A plugin registered on one container has no effect on another.