Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"eslint": "^8.57.1",
"eslint-config-reearth": "^0.2.1",
"prettier": "^2.8.8",
"quickjs-emscripten": "^0.25.0",
"quickjs-emscripten": "^0.29.0",
"typescript": "^5.7.2",
"vite": "^6.0.3",
"vite-plugin-dts": "^4.3.0",
Expand Down
121 changes: 121 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,107 @@ describe("evalModule", () => {
arena.dispose();
ctx.dispose();
});

test("module returns exported values (0.29+)", async () => {
const ctx = (await getQuickJS()).newContext();
const arena = new Arena(ctx, { isMarshalable: true });

const exports = arena.evalModule(`
export const value = 42;
export const message = "Hello";
export const obj = { a: 1, b: 2 };
`);

expect(exports.value).toBe(42);
expect(exports.message).toBe("Hello");
expect(exports.obj).toEqual({ a: 1, b: 2 });

arena.dispose();
ctx.dispose();
});

test("module returns exported functions (0.29+)", async () => {
const ctx = (await getQuickJS()).newContext();
const arena = new Arena(ctx, { isMarshalable: true });

const exports = arena.evalModule(`
export function greet(name) {
return "Hello, " + name;
}
export function add(a, b) {
return a + b;
}
`);

expect(exports.greet("World")).toBe("Hello, World");
expect(exports.add(2, 3)).toBe(5);

arena.dispose();
ctx.dispose();
});

test("module with default export (0.29+)", async () => {
const ctx = (await getQuickJS()).newContext();
const arena = new Arena(ctx, { isMarshalable: true });

const exports = arena.evalModule(`
export default function(x) {
return x * 2;
}
`);

expect(exports.default(21)).toBe(42);

arena.dispose();
ctx.dispose();
});

test("module with class export (0.29+)", async () => {
const ctx = (await getQuickJS()).newContext();
const arena = new Arena(ctx, { isMarshalable: true });

// Export objects and static methods from a class
const exports = arena.evalModule(`
export class Counter {
static create(initial = 0) {
return { count: initial };
}
static increment(obj) {
return ++obj.count;
}
}
`);

// Static methods can be called from the host
const counter = exports.Counter.create(10);
expect(counter.count).toBe(10);
expect(exports.Counter.increment(counter)).toBe(11);
expect(counter.count).toBe(11);

arena.dispose();
ctx.dispose();
});

test("module with top-level await (0.29+)", async () => {
const ctx = (await getQuickJS()).newContext();
const arena = new Arena(ctx, { isMarshalable: true });

const exportsPromise = arena.evalModule(`
export const data = await Promise.resolve(123);
export const message = "loaded";
`);

expect(exportsPromise).toBeInstanceOf(Promise);

arena.executePendingJobs();

const exports = await exportsPromise;
expect(exports.data).toBe(123);
expect(exports.message).toBe("loaded");

arena.dispose();
ctx.dispose();
});
});

describe("memory management", () => {
Expand Down Expand Up @@ -911,3 +1012,23 @@ describe("memory management", () => {
ctx.dispose();
});
});

describe("intrinsics configuration", () => {
test("intrinsics can be configured when creating context", async () => {
const quickjs = await getQuickJS();
const runtime = quickjs.newRuntime();

// Example: disable eval for sandboxing
const ctx = runtime.newContext({ intrinsics: { Eval: false } });
const arena = new Arena(ctx, { isMarshalable: true });

// This test demonstrates that intrinsics are configured at context creation
// The actual restrictions would be enforced by quickjs-emscripten
expect(arena).toBeDefined();
expect(arena.context).toBeDefined();

arena.dispose();
ctx.dispose();
runtime.dispose();
});
});
37 changes: 28 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
QuickJSContext,
SuccessOrFail,
VmCallResult,
Intrinsics,
} from "quickjs-emscripten";

import { wrapContext, QuickJSContextEx } from "./contextex";
Expand Down Expand Up @@ -31,6 +32,8 @@ export {
consumeAll,
};

export type { Intrinsics };

export type Options = {
/** A callback that returns a boolean value that determines whether an object is marshalled or not. If false, no marshaling will be done and undefined will be passed to the QuickJS VM, otherwise marshaling will be done. By default, all objects will be marshalled. */
isMarshalable?: boolean | "json" | ((target: any) => boolean | "json");
Expand Down Expand Up @@ -104,24 +107,40 @@ export class Arena {
}

/**
* Evaluate ES module code in the VM. The module can import/export, but the return value
* depends on whether the module exports are accessible (implementation varies by quickjs-emscripten version).
* Use this for side effects or when you don't need access to exports.
* Evaluate ES module code in the VM and get the module's exports.
*
* Requires quickjs-emscripten >= 0.29.0 for export access.
*
* @param code - The ES module code to evaluate
* @param filename - Optional filename for debugging purposes (default: "module.js")
* @returns Undefined in most cases (module side effects are applied)
* @returns The module's exports object, or a Promise resolving to exports if using top-level await
*
* @example
* ```js
* // Execute module code with side effects
* arena.expose({ data: { count: 0 } });
* arena.evalModule('import { data } from "globals"; data.count = 42;'); // undefined, but data.count is now 42
* // Simple module with exports
* const exports = arena.evalModule(`
* export const value = 42;
* export function greet(name) {
* return "Hello, " + name;
* }
* `);
* console.log(exports.value); // 42
* console.log(exports.greet("World")); // "Hello, World"
*
* // Module with default export
* const mod = arena.evalModule('export default function(x) { return x * 2; }');
* console.log(mod.default(21)); // 42
*
* // Module with top-level await
* const promise = arena.evalModule('export const data = await Promise.resolve(123);');
* arena.executePendingJobs();
* const exports = await promise;
* console.log(exports.data); // 123
* ```
*/
evalModule(code: string, filename = "module.js"): void {
evalModule<T = any>(code: string, filename = "module.js"): T | Promise<T> {
const handle = this.context.evalCode(code, filename, { type: "module" });
this._unwrapResultAndUnmarshal(handle);
return this._unwrapResultAndUnmarshal(handle);
}

/**
Expand Down
6 changes: 4 additions & 2 deletions src/vmutil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ export function fn(
code: string,
): ((thisArg: QuickJSHandle | undefined, ...args: QuickJSHandle[]) => QuickJSHandle) & Disposable {
const handle = ctx.unwrapResult(ctx.evalCode(code));
const f = (thisArg: QuickJSHandle | undefined, ...args: QuickJSHandle[]): any => {
const f: any = (thisArg: QuickJSHandle | undefined, ...args: QuickJSHandle[]): any => {
return ctx.unwrapResult(ctx.callFunction(handle, thisArg ?? ctx.undefined, ...args));
};
f.dispose = () => handle.dispose();
const disposeFn = () => handle.dispose();
f.dispose = disposeFn;
f[Symbol.dispose] = disposeFn;
f.alive = true;
Object.defineProperty(f, "alive", {
get: () => handle.alive,
Expand Down
76 changes: 38 additions & 38 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -340,38 +340,38 @@
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==

"@jitl/quickjs-ffi-types@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@jitl/quickjs-ffi-types/-/quickjs-ffi-types-0.25.1.tgz#d1cea176dd818f8d2ea6b48a9f18e129921e4172"
integrity sha512-m3HqjAsG3DbCwqelRMQYyPU2uGF4f+CAmEn9VhhbK6YXmsUCf0mrULmr4wuYt75xx5NIDcBFvjLJ1303pwGoyg==
"@jitl/quickjs-ffi-types@0.29.2":
version "0.29.2"
resolved "https://registry.yarnpkg.com/@jitl/quickjs-ffi-types/-/quickjs-ffi-types-0.29.2.tgz#91aaa6be43471b5c8fc3a85f94582c86d345ba2e"
integrity sha512-069uQTiEla2PphXg6UpyyJ4QXHkTj3S9TeXgaMCd8NDYz3ODBw5U/rkg6fhuU8SMpoDrWjEzybmV5Mi2Pafb5w==

"@jitl/quickjs-wasmfile-debug-asyncify@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-debug-asyncify/-/quickjs-wasmfile-debug-asyncify-0.25.1.tgz#240c927d5f37bede93f7944f560854d6de67e244"
integrity sha512-0+p3C8MxRrF9/B1saRe/QSxdPGPSORuNRqOIL1eYyDNllB3E32+StUjtO+ut3YwhktQEgqaacc/pmHYr548cjg==
"@jitl/quickjs-wasmfile-debug-asyncify@0.29.2":
version "0.29.2"
resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-debug-asyncify/-/quickjs-wasmfile-debug-asyncify-0.29.2.tgz#ddf58d1ab90b94a70a73b93f95c4e17f16d0a2a9"
integrity sha512-YdRw2414pFkxzyyoJGv81Grbo9THp/5athDMKipaSBNNQvFE9FGRrgE9tt2DT2mhNnBx1kamtOGj0dX84Yy9bg==
dependencies:
"@jitl/quickjs-ffi-types" "0.25.1"
"@jitl/quickjs-ffi-types" "0.29.2"

"@jitl/quickjs-wasmfile-debug-sync@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-debug-sync/-/quickjs-wasmfile-debug-sync-0.25.1.tgz#3d1e4b1439abf2bfe682acc90c631f93c8c9fed5"
integrity sha512-Xi8m5V2rGikRAW08kX/9FQOHtfBIzLLuRVKFDSeJ30qqh8aY07wYvc2nqfhOOLnLYMzZkEv3iyIPN/HHJ6ADkw==
"@jitl/quickjs-wasmfile-debug-sync@0.29.2":
version "0.29.2"
resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-debug-sync/-/quickjs-wasmfile-debug-sync-0.29.2.tgz#b1a9c0f9371de2e8e67f8d025c0d9c9b7360f175"
integrity sha512-VgisubjyPMWEr44g+OU0QWGyIxu7VkApkLHMxdORX351cw22aLTJ+Z79DJ8IVrTWc7jh4CBPsaK71RBQDuVB7w==
dependencies:
"@jitl/quickjs-ffi-types" "0.25.1"
"@jitl/quickjs-ffi-types" "0.29.2"

"@jitl/quickjs-wasmfile-release-asyncify@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-release-asyncify/-/quickjs-wasmfile-release-asyncify-0.25.1.tgz#0c8e1547f1e3608de7483e60db680323084082a7"
integrity sha512-0jexwropCT11renRczAY/w+7ZMn+m3NEfXRjVeg6OMRk1volHG4zlhcxZQWhF5Ts0yphHpoTPM73JI9Q8/lohQ==
"@jitl/quickjs-wasmfile-release-asyncify@0.29.2":
version "0.29.2"
resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-release-asyncify/-/quickjs-wasmfile-release-asyncify-0.29.2.tgz#51c23bfd93d56d9eaab47ee937838c91376b093a"
integrity sha512-sf3luCPr8wBVmGV6UV8Set+ie8wcO6mz5wMvDVO0b90UVCKfgnx65A1JfeA+zaSGoaFyTZ3sEpXSGJU+6qJmLw==
dependencies:
"@jitl/quickjs-ffi-types" "0.25.1"
"@jitl/quickjs-ffi-types" "0.29.2"

"@jitl/quickjs-wasmfile-release-sync@0.25.1":
version "0.25.1"
resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-release-sync/-/quickjs-wasmfile-release-sync-0.25.1.tgz#7e2159977988bccfce6c0ac9b788423ddaf1f3ef"
integrity sha512-dYn7iZWR9Z1awCy8GQiX0dfnBpbiRFU5UUlLAyDk4xyku7URAKb3fhQOESY4h6s0kRRz367KaDHr25nRFHmOtA==
"@jitl/quickjs-wasmfile-release-sync@0.29.2":
version "0.29.2"
resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-release-sync/-/quickjs-wasmfile-release-sync-0.29.2.tgz#7a9a055739bfc6261203701a46e2c627cfabe6f2"
integrity sha512-UFIcbY3LxBRUjEqCHq3Oa6bgX5znt51V5NQck8L2US4u989ErasiMLUjmhq6UPC837Sjqu37letEK/ZpqlJ7aA==
dependencies:
"@jitl/quickjs-ffi-types" "0.25.1"
"@jitl/quickjs-ffi-types" "0.29.2"

"@jridgewell/resolve-uri@^3.1.0":
version "3.1.2"
Expand Down Expand Up @@ -2683,23 +2683,23 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==

quickjs-emscripten-core@0.25.1:
version "0.25.1"
resolved "https://registry.yarnpkg.com/quickjs-emscripten-core/-/quickjs-emscripten-core-0.25.1.tgz#caa22b3674f08618b885008f0b1fb213eb79c94d"
integrity sha512-kUY9gPdxrLA4c9Erc69RgXL7ZZ20zti2MYK0YL/ADIzRK88oidZhuzO0ybf4VPis7WCO7cVsMfR/FYVviI2iaA==
quickjs-emscripten-core@0.29.2:
version "0.29.2"
resolved "https://registry.yarnpkg.com/quickjs-emscripten-core/-/quickjs-emscripten-core-0.29.2.tgz#ad9bef103fce7d8ba879510905824e33119a62f3"
integrity sha512-jEAiURW4jGqwO/fW01VwlWqa2G0AJxnN5FBy1xnVu8VIVhVhiaxUfCe+bHqS6zWzfjFm86HoO40lzpteusvyJA==
dependencies:
"@jitl/quickjs-ffi-types" "0.25.1"
"@jitl/quickjs-ffi-types" "0.29.2"

quickjs-emscripten@^0.25.0:
version "0.25.1"
resolved "https://registry.yarnpkg.com/quickjs-emscripten/-/quickjs-emscripten-0.25.1.tgz#7227bf7a2e0116224a75226373360be3d2aa1e52"
integrity sha512-9HsI5KkrzWyKSTiAs+tZ+l48ETqqblG/L+Mm+4qIUwySns0lR1Ik5CFU06pDmUZbk5JooDgakd0bVfDLOQNJRg==
quickjs-emscripten@^0.29.0:
version "0.29.2"
resolved "https://registry.yarnpkg.com/quickjs-emscripten/-/quickjs-emscripten-0.29.2.tgz#da6c7afb543cc5b4854eaca457778cfae5069f8b"
integrity sha512-SlvkvyZgarReu2nr4rkf+xz1vN0YDUz7sx4WHz8LFtK6RNg4/vzAGcFjE7nfHYBEbKrzfIWvKnMnxZkctQ898w==
dependencies:
"@jitl/quickjs-wasmfile-debug-asyncify" "0.25.1"
"@jitl/quickjs-wasmfile-debug-sync" "0.25.1"
"@jitl/quickjs-wasmfile-release-asyncify" "0.25.1"
"@jitl/quickjs-wasmfile-release-sync" "0.25.1"
quickjs-emscripten-core "0.25.1"
"@jitl/quickjs-wasmfile-debug-asyncify" "0.29.2"
"@jitl/quickjs-wasmfile-debug-sync" "0.29.2"
"@jitl/quickjs-wasmfile-release-asyncify" "0.29.2"
"@jitl/quickjs-wasmfile-release-sync" "0.29.2"
quickjs-emscripten-core "0.29.2"

react-is@^16.13.1:
version "16.13.1"
Expand Down