Custom Integrations
Build your own integrations to extend VMs.
You can create custom integrations by extending the VmWith class. Integrations can provide any configuration you would otherwise pass when creating a VM, plus helper methods for interacting with the VM.
import { freestyle } from "freestyle";
import { VmNodeJs } from "@freestyle-sh/with-nodejs";
import { VmPython } from "@freestyle-sh/with-python";
const { vm } = await freestyle.vms.create({
with: {
js: new VmNodeJs(),
python: new VmPython(),
},
});
// the properties defined under `with` are now available on the vm class.
await vm.js.run(`console.log("Hello World!")`);Importantly, these classes encapsulate their own caching logic via snapshot specs.
import { freestyle } from "freestyle";
import { VmNodeJs } from "@freestyle-sh/with-nodejs";
// first time is slow because this configuration has never been created and cached before
const { vm } = await freestyle.vms.create({
with: {
js: new VmNodeJs({ nodeVersion: "20"}),
},
});
// second time is fast because the configuration has been cached
const { vm: vm2 } = await freestyle.vms.create({
with: {
js: new VmNodeJs({ nodeVersion: "20" }),
},
});How it works
VmWith uses a builder pattern to compose VM configurations like middleware. Each
VmWith class has two main responsibilities:
1. Configuration (configureSpec / configureSnapshotSpec methods)
The configureSpec and configureSnapshotSpec methods transform VM specs by
merging your component's settings with existing configuration. They receive the
current VmSpec and return a modified version with your changes applied.
A common pattern is to put most of your configuration inside VmSpec and
apply it via configureSnapshotSpec so it can be cached when used as a
snapshot layer. For example with NodeJs, we can add run an installation
script via systemd inside the spec. For more details on caching, see Specs
and Snapshots.
class VmNodeJs extends VmWith<NodeJsRuntimeInstance> {
async configureSnapshotSpec(spec: VmSpec): Promise<VmSpec> {
return spec
.additionalFiles({
"/opt/install-nodejs.sh": { content: installScript },
"/etc/profile.d/nvm.sh": { content: nvmInit },
})
.systemdService({
name: "install-nodejs",
mode: "oneshot",
exec: ["bash /opt/install-nodejs.sh"],
});
}
}Builder methods mutate and return the same VmSpec instance, so this pattern
applies your integration's configuration directly to the incoming spec.
2. Runtime Instance
The createInstance method returns an instance that will be attached to the VM,
providing convenient interaction methods. This instance has access to the VM via
the vm property.
class NodeJsRuntimeInstance extends VmWithInstance {
async exec(script: string) {
return await this.vm.exec({
command: `npm run '${script}'`,
});
}
}
class VmNodeJs extends VmWith<NodeJsRuntimeInstance> {
createInstance(): NodeJsRuntimeInstance {
return new NodeJsRuntimeInstance();
}
}Dependencies and Generics
If you're building a VmWith that depends on another VmWith, we recommend requiring it in the constructor. For example, a VmDevServer depends on the generic VmJavaScriptRuntime so that users choose between NodeJs, Deno, Bun, etc.
const js = new VmNodeJs();
const devServer = new VmDevServer({
placeholderRepo: "https://github.com/example/placeholder",
repo: "https://github.com/example/app",
runtime: js,
});
const { vm } = await freestyle.vms.create({
with: {
js,
devServer,
},
});Internally, VmDevServer can then use the runtime's helper methods to configure it's own installation correctly.
export class VmDevServer extends VmWith {
constructor(private options: { runtime: VmJavaScriptRuntime }) { super() }
async configureSnapshotSpec(spec: VmSpec): Promise<VmSpec> {
return spec.systemdService({
name: "dev-server-install-dependencies",
mode: "oneshot",
// use the install command provided by the runtime
exec: [this.options.runtime.installCommand()],
// ensure we don't run npm install before the runtime is installed
after: [this.options.runtime.installServiceName()],
// ...
});
}
// ...
}If you need to access a dependencies' instance, you can do so using the
.instance property. This will only be available after the VM has been created,
so you must read it inside a VmWithInstance.
class VmDevServer extends VmWith {
// ...
createInstance(): VmDevServerInstance {
return new VmDevServerInstance(
this.options.runtime
);
}
}
class VmDevServerInstance extends VmWithInstance {
constructor(public runtime: VmJavaScriptRuntime) {
super();
}
helloWorld() {
return this.runtime.instance.runCode("console.log('Hello World!')");
}
}