Skip to main content
Get Started
Node.js Runtime

NPM & Module Loading

How sandboxed code resolves and loads modules.

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.

Loading Modules

  • 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.
// Boot a fully virtualized VM. Module resolution runs entirely inside the
// kernel - `import` and `require` resolve against the guest's virtual
// filesystem, never the host's.
const rt = await NodeRuntime.create();

try {
  // Guest code runs as an ES module inside the VM, so it can `import` Node
  // builtins directly. It can also build a CommonJS `require` with
  // `createRequire` to load builtins the classic way. Both resolve through the
  // kernel's module loader.
  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();
}
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"}
Resolution runs inside the kernel against the guest’s virtual filesystem. The guest only sees what is present there; mounting host node_modules (below) makes those packages part of that filesystem so they resolve like any other guest module.

Loading real npm packages

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

  • create({ nodeModules }): a convenience for a read-only host-directory mount that projects a whole host node_modules tree in one call. It uses the same mount machinery as mounts, defaulting to guest /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.

// Point `nodeModules` at a host `node_modules` directory and the whole tree is
// projected into the VM in one call. Any package inside resolves the way Node
// would over a real filesystem, symlinks and all. Here we mount this repo's
// root node_modules, which includes the tiny `is-number` package.
const hostNodeModules = fileURLToPath(
  new URL("../../../../node_modules", import.meta.url),
);

const mounted = await NodeRuntime.create({
  nodeModules: hostNodeModules,
});

try {
  // The guest resolves `is-number` from the mounted host node_modules the same
  // way Node would over a real filesystem, then uses the real package's code.
  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();
}
exitCode: 0
guest stdout:
loaded real npm package is-number
{"isNumber(42)":true,"isNumber(\"3.14\")":true,"isNumber(\"nope\")":false,"sameModule":true}

node_modules resolution

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.

A nodeModules (or mounts) host mount confines reads to the mounted directory: symlinks are followed only while they stay inside the mount root. A single-project npm/pnpm install resolves fine because its symlinks (including the .pnpm store) live inside node_modules. A workspace/monorepo install is different: pnpm/yarn and file: deps symlink packages to the workspace root or an external store, outside the leaf node_modules. Those targets are not followed, and the guest import fails with Cannot resolve module '<pkg>': not found. Fix it by pointing the mount at a directory that contains every symlink target (for example the workspace root, mounted at the guest path the program resolves from) instead of the leaf node_modules.

Seeding files directly

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");