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.
Loading Modules
Section titled “Loading Modules”- Guest source runs as a standard ES module:
import,import.meta.url, and top-levelawaitall work. - Build a CommonJS
requirewithcreateRequire(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();}exitCode: 0guest stdout:resolved node:path via import -> /workspace/data/report.txtresolved node:os via require -> linux{"basename":"report.txt","joined":"/workspace/data/report.txt","platform":"linux"}Loading real npm packages
Section titled “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 }): project a whole hostnode_modulestree in one call. Read-only, defaulting to/tmp/node_modules, which is where the resolution walk begins for a program run byexec()/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 amountsentry per package when you want fine-grained control instead of the whole tree.create({ files })orrt.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();}exitCode: 0guest stdout:loaded real npm package is-number{"isNumber(42)":true,"isNumber(\"3.14\")":true,"isNumber(\"nope\")":false,"sameModule":true}node_modules resolution
Section titled “node_modules resolution”When a guest filesystem contains node_modules, the resolver matches naive Node.js resolution over it, Docker-style:
- ancestor
node_moduleswalk from the importing module up to the root, package.jsonexports/importsand 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.
Seeding files directly
Section titled “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");