From dd3344bf0a23b4df391b3d4e1d2d465f385bf683 Mon Sep 17 00:00:00 2001 From: Mavdol Date: Thu, 2 Apr 2026 22:03:42 +0200 Subject: [PATCH 1/2] implement module import support in js_sandbox via registry and import rewriting --- .../src/js_sandbox/execution.ts | 45 ++++++++++++++++++- .../src/js_sandbox/serialization.ts | 18 ++++++++ .../src/js_sandbox/execution.ts | 45 ++++++++++++++++++- .../src/js_sandbox/serialization.ts | 18 ++++++++ 4 files changed, 124 insertions(+), 2 deletions(-) diff --git a/integrations/python-adapter/src/js_sandbox/execution.ts b/integrations/python-adapter/src/js_sandbox/execution.ts index 609273e..8a8743e 100644 --- a/integrations/python-adapter/src/js_sandbox/execution.ts +++ b/integrations/python-adapter/src/js_sandbox/execution.ts @@ -1,3 +1,7 @@ +import { fs, path, os, process } from "@capsule-run/sdk"; + +const fsPromises = fs.promises; + function hoistDeclarations(code: string): string { let depth = 0; let i = 0; @@ -126,8 +130,41 @@ function hoistDeclarations(code: string): string { return out; } +/** + * Maps module names user code can import to their sandbox equivalents. + * These are the WASI polyfills from the capsule SDK. + */ +export const MODULE_REGISTRY: Record = { + "fs": fs, + "fs/promises": fsPromises, + "node:fs": fs, + "node:fs/promises": fsPromises, + "path": path, + "node:path": path, + "os": os, + "node:os": os, + "process": process, + "node:process": process, +}; + +export const MODULE_VALUES: Set = new Set(Object.values(MODULE_REGISTRY)); +export const TRANSIENT_KEYS: Set = new Set(["__require__", "require"]); + + +function rewriteImports(code: string): string { + return code.replace( + /^[ \t]*import\s+(?:(\*\s+as\s+\w+|\{[^}]*\}|\w+)\s+from\s+)?['"]([^'"]+)['"]\s*;?/gm, + (_, binding: string | undefined, mod: string) => { + if (!binding) return `__require__('${mod}');`; + const normalized = binding.replace(/^\*\s+as\s+/, ""); + return `var ${normalized} = __require__('${mod}');`; + } + ); +} + export function _executeCode(code: string, env: Record): unknown { - code = hoistDeclarations(code); + code = rewriteImports(hoistDeclarations(code)); + const capturedOutput: string[] = []; const originalLog = console.log; @@ -136,6 +173,12 @@ export function _executeCode(code: string, env: Record): unknow }; try { + env.__require__ = (mod: string): unknown => { + if (mod in MODULE_REGISTRY) return MODULE_REGISTRY[mod]; + throw new Error(`Cannot import '${mod}': module is not available in the sandbox`); + }; + env.require = env.__require__; + const proxy = new Proxy(env, { has(_t, _k) { return true; }, get(t, k) { diff --git a/integrations/python-adapter/src/js_sandbox/serialization.ts b/integrations/python-adapter/src/js_sandbox/serialization.ts index 57cda89..490f904 100644 --- a/integrations/python-adapter/src/js_sandbox/serialization.ts +++ b/integrations/python-adapter/src/js_sandbox/serialization.ts @@ -1,3 +1,5 @@ +import { MODULE_VALUES, TRANSIENT_KEYS, MODULE_REGISTRY } from "./execution"; + export type SerializedValue = | { __type__: "primitive"; value: string | number | boolean | null } | { __type__: "undefined" } @@ -13,6 +15,7 @@ export type SerializedValue = | { __type__: "classdef"; __source__: string } | { __type__: "function"; __source__: string } | { __type__: "instance"; __class__: string; __source__: string; __dict__: Record } + | { __type__: "module"; name: string } // use for import/require() modules | null; export function serializeValue(val: unknown): SerializedValue { @@ -96,16 +99,29 @@ export function serializeValue(val: unknown): SerializedValue { return null; } +const MODULE_VALUE_TO_NAME: Map = new Map( + Object.entries(MODULE_REGISTRY) + .filter(([name]) => !name.startsWith("node:")) + .map(([name, val]) => [val, name]) +); + export function serializeEnv(env: Record): Record { const out: Record = {}; for (const [key, val] of Object.entries(env)) { if (key.startsWith("__")) continue; + if (TRANSIENT_KEYS.has(key)) continue; + if (MODULE_VALUES.has(val)) { + const name = MODULE_VALUE_TO_NAME.get(val); + if (name) out[key] = { __type__: "module", name }; + continue; + } const s = serializeValue(val); if (s !== null) out[key] = s; } return out; } + export function deserializeValue( entry: SerializedValue, classes: Record unknown> @@ -153,6 +169,8 @@ export function deserializeValue( } return instance; } + case "module": + return MODULE_REGISTRY[entry.name]; default: return undefined; } diff --git a/integrations/typescript-adapter/src/js_sandbox/execution.ts b/integrations/typescript-adapter/src/js_sandbox/execution.ts index 609273e..8a8743e 100644 --- a/integrations/typescript-adapter/src/js_sandbox/execution.ts +++ b/integrations/typescript-adapter/src/js_sandbox/execution.ts @@ -1,3 +1,7 @@ +import { fs, path, os, process } from "@capsule-run/sdk"; + +const fsPromises = fs.promises; + function hoistDeclarations(code: string): string { let depth = 0; let i = 0; @@ -126,8 +130,41 @@ function hoistDeclarations(code: string): string { return out; } +/** + * Maps module names user code can import to their sandbox equivalents. + * These are the WASI polyfills from the capsule SDK. + */ +export const MODULE_REGISTRY: Record = { + "fs": fs, + "fs/promises": fsPromises, + "node:fs": fs, + "node:fs/promises": fsPromises, + "path": path, + "node:path": path, + "os": os, + "node:os": os, + "process": process, + "node:process": process, +}; + +export const MODULE_VALUES: Set = new Set(Object.values(MODULE_REGISTRY)); +export const TRANSIENT_KEYS: Set = new Set(["__require__", "require"]); + + +function rewriteImports(code: string): string { + return code.replace( + /^[ \t]*import\s+(?:(\*\s+as\s+\w+|\{[^}]*\}|\w+)\s+from\s+)?['"]([^'"]+)['"]\s*;?/gm, + (_, binding: string | undefined, mod: string) => { + if (!binding) return `__require__('${mod}');`; + const normalized = binding.replace(/^\*\s+as\s+/, ""); + return `var ${normalized} = __require__('${mod}');`; + } + ); +} + export function _executeCode(code: string, env: Record): unknown { - code = hoistDeclarations(code); + code = rewriteImports(hoistDeclarations(code)); + const capturedOutput: string[] = []; const originalLog = console.log; @@ -136,6 +173,12 @@ export function _executeCode(code: string, env: Record): unknow }; try { + env.__require__ = (mod: string): unknown => { + if (mod in MODULE_REGISTRY) return MODULE_REGISTRY[mod]; + throw new Error(`Cannot import '${mod}': module is not available in the sandbox`); + }; + env.require = env.__require__; + const proxy = new Proxy(env, { has(_t, _k) { return true; }, get(t, k) { diff --git a/integrations/typescript-adapter/src/js_sandbox/serialization.ts b/integrations/typescript-adapter/src/js_sandbox/serialization.ts index 57cda89..490f904 100644 --- a/integrations/typescript-adapter/src/js_sandbox/serialization.ts +++ b/integrations/typescript-adapter/src/js_sandbox/serialization.ts @@ -1,3 +1,5 @@ +import { MODULE_VALUES, TRANSIENT_KEYS, MODULE_REGISTRY } from "./execution"; + export type SerializedValue = | { __type__: "primitive"; value: string | number | boolean | null } | { __type__: "undefined" } @@ -13,6 +15,7 @@ export type SerializedValue = | { __type__: "classdef"; __source__: string } | { __type__: "function"; __source__: string } | { __type__: "instance"; __class__: string; __source__: string; __dict__: Record } + | { __type__: "module"; name: string } // use for import/require() modules | null; export function serializeValue(val: unknown): SerializedValue { @@ -96,16 +99,29 @@ export function serializeValue(val: unknown): SerializedValue { return null; } +const MODULE_VALUE_TO_NAME: Map = new Map( + Object.entries(MODULE_REGISTRY) + .filter(([name]) => !name.startsWith("node:")) + .map(([name, val]) => [val, name]) +); + export function serializeEnv(env: Record): Record { const out: Record = {}; for (const [key, val] of Object.entries(env)) { if (key.startsWith("__")) continue; + if (TRANSIENT_KEYS.has(key)) continue; + if (MODULE_VALUES.has(val)) { + const name = MODULE_VALUE_TO_NAME.get(val); + if (name) out[key] = { __type__: "module", name }; + continue; + } const s = serializeValue(val); if (s !== null) out[key] = s; } return out; } + export function deserializeValue( entry: SerializedValue, classes: Record unknown> @@ -153,6 +169,8 @@ export function deserializeValue( } return instance; } + case "module": + return MODULE_REGISTRY[entry.name]; default: return undefined; } From bc4355ed50241767eb7f8b18129d2efda1c27f7c Mon Sep 17 00:00:00 2001 From: Mavdol Date: Thu, 2 Apr 2026 23:26:54 +0200 Subject: [PATCH 2/2] bump typescript-adapter version to 0.3.5 and update dependency lockfiles --- integrations/python-adapter/pyproject.toml | 2 +- integrations/typescript-adapter/package-lock.json | 4 ++-- integrations/typescript-adapter/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/integrations/python-adapter/pyproject.toml b/integrations/python-adapter/pyproject.toml index cfe8f2a..15092e7 100644 --- a/integrations/python-adapter/pyproject.toml +++ b/integrations/python-adapter/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "capsule-run-adapter" -version = "0.3.4" +version = "0.3.5" description = "Capsule adapter for Python applications — execute Python and JavaScript code in isolated WebAssembly sandboxes" license = { text = "Apache-2.0" } readme = "README.md" diff --git a/integrations/typescript-adapter/package-lock.json b/integrations/typescript-adapter/package-lock.json index 1698582..f7540e4 100644 --- a/integrations/typescript-adapter/package-lock.json +++ b/integrations/typescript-adapter/package-lock.json @@ -1,12 +1,12 @@ { "name": "@capsule-run/adapter", - "version": "0.3.4", + "version": "0.3.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@capsule-run/adapter", - "version": "0.3.4", + "version": "0.3.5", "license": "Apache-2.0", "dependencies": { "@capsule-run/cli": "latest", diff --git a/integrations/typescript-adapter/package.json b/integrations/typescript-adapter/package.json index de97509..89e1c40 100644 --- a/integrations/typescript-adapter/package.json +++ b/integrations/typescript-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@capsule-run/adapter", - "version": "0.3.4", + "version": "0.3.5", "description": "Capsule adapter for typescript applications", "license": "Apache-2.0", "author": "",