Skip to content
GitHub Get Started
Node.js Runtime

NPM & Module Loading

Guest import and require resolve against the VM’s virtual filesystem, never the host module loader. Resolution runs entirely inside the kernel. By default the guest sees an empty node_modules; project host packages into the VM with nodeModules (or mounts) to run real npm packages (including the TypeScript compiler) in-sandbox.

  • Guest source runs as a standard ES module: import, import.meta.url, and top-level await all work.
  • Build a CommonJS require with createRequire(import.meta.url).
  • Both paths resolve through the kernel’s module loader.
import { NodeRuntime } from "secure-exec";
const rt = await NodeRuntime.create();
try {
const { stdout, stderr, exitCode } = await rt.exec(`
// ESM import of a Node builtin.
import { basename, join } from "node:path";
// CommonJS require, created from the current module URL.
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const os = require("node:os");
const resolved = {
basename: basename("/workspace/data/report.txt"),
joined: join("/workspace", "data", "report.txt"),
platform: os.platform(),
};
console.log("resolved node:path via import ->", resolved.joined);
console.log("resolved node:os via require ->", resolved.platform);
console.log(JSON.stringify(resolved));
`);
console.log("exitCode:", exitCode);
if (stderr.trim()) console.log("guest stderr:\n" + stderr.trim());
console.log("guest stdout:");
console.log(stdout.trim());
} finally {
await rt.dispose();
}

See Full Example

exitCode: 0
guest stdout:
resolved node:path via import -> /workspace/data/report.txt
resolved node:os via require -> linux
{"basename":"report.txt","joined":"/workspace/data/report.txt","platform":"linux"}

Put package bytes on the guest filesystem, then let the in-kernel resolver walk them. Three ways to project them:

  • create({ nodeModules }): project a whole host node_modules tree in one call. Read-only, defaulting to /tmp/node_modules, which is where the resolution walk begins for a program run by exec() / run() (each program is written under /tmp). Pass the object form ({ hostPath, guestPath }) to mount it elsewhere.
  • create({ mounts }): project one host directory at a time onto a guest path, Docker-style. Use a mounts entry per package when you want fine-grained control instead of the whole tree.
  • create({ files }) or rt.writeFile: write bytes directly into the VM when you want to seed files instead of projecting a host tree.

Either way the host filesystem is never exposed; the guest sees only the projected subtree or the bytes you write. For the full mount shapes see the TypeScript SDK reference.

The example below points nodeModules at the host directory that holds is-number, then imports it from inside the VM. The guest resolves it the way Node would over a real filesystem, including through createRequire.

import { fileURLToPath } from "node:url";
import { NodeRuntime } from "secure-exec";
const hostNodeModules = fileURLToPath(
new URL("../../../../node_modules", import.meta.url),
);
const mounted = await NodeRuntime.create({
nodeModules: hostNodeModules,
});
try {
const { stdout, stderr, exitCode } = await mounted.exec(`
// ESM import of the real, host-mounted npm package.
import isNumber from "is-number";
// The same package also resolves through a CommonJS require.
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const isNumberCjs = require("is-number");
const result = {
"isNumber(42)": isNumber(42),
'isNumber("3.14")': isNumber("3.14"),
'isNumber("nope")': isNumber("nope"),
sameModule: isNumber === isNumberCjs,
};
console.log("loaded real npm package is-number");
console.log(JSON.stringify(result));
`);
console.log("exitCode:", exitCode);
if (stderr.trim()) console.log("guest stderr:\n" + stderr.trim());
console.log("guest stdout:");
console.log(stdout.trim());
} finally {
await mounted.dispose();
}

See Full Example

exitCode: 0
guest stdout:
loaded real npm package is-number
{"isNumber(42)":true,"isNumber(\"3.14\")":true,"isNumber(\"nope\")":false,"sameModule":true}

When a guest filesystem contains node_modules, the resolver matches naive Node.js resolution over it, Docker-style:

  • ancestor node_modules walk from the importing module up to the root,
  • package.json exports/imports and conditions,
  • realpath/symlink following.

No package-manager-specific heuristics: pnpm/yarn layouts resolve because the VFS exposes their symlinks, not because the resolver special-cases them.

When you don’t have a host directory to mount, write bytes into the VM:

// At boot.
const rt = await NodeRuntime.create({
files: { "/tmp/node_modules/greet/index.js": "module.exports = () => 'hi';" },
});
// Or after boot.
await rt.writeFile("/tmp/node_modules/greet/package.json", '{"main":"index.js"}');
const bytes = await rt.readFile("/tmp/node_modules/greet/index.js");