quickjs-emscripten-sync 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.test.ts CHANGED
@@ -73,6 +73,70 @@ describe("readme", () => {
73
73
  });
74
74
  });
75
75
 
76
+ describe("class constructors (#92)", () => {
77
+ test("new on an exposed class inside the VM", async () => {
78
+ const ctx = (await getQuickJS()).newContext();
79
+ const arena = new Arena(ctx, { isMarshalable: true });
80
+
81
+ class Cls {
82
+ hoge = "";
83
+
84
+ constructor() {
85
+ this.hoge = "foo";
86
+ }
87
+ }
88
+ arena.expose({ Cls });
89
+
90
+ const instance = arena.evalCode(`new Cls()`) as { hoge: string };
91
+ expect(instance.hoge).toBe("foo");
92
+ expect(arena.evalCode(`new Cls() instanceof Cls`)).toBe(true);
93
+
94
+ arena.dispose();
95
+ ctx.dispose();
96
+ });
97
+
98
+ test("constructor arguments reach the host", async () => {
99
+ const ctx = (await getQuickJS()).newContext();
100
+ const arena = new Arena(ctx, { isMarshalable: true });
101
+
102
+ class Cls {
103
+ value: number;
104
+
105
+ constructor(a: number, b: number) {
106
+ this.value = a + b;
107
+ }
108
+ }
109
+ arena.expose({ Cls });
110
+
111
+ const instance = arena.evalCode(`new Cls(2, 3)`) as { value: number };
112
+ expect(instance.value).toBe(5);
113
+
114
+ arena.dispose();
115
+ ctx.dispose();
116
+ });
117
+
118
+ test("new on a synced class inside the VM", async () => {
119
+ const ctx = (await getQuickJS()).newContext();
120
+ const arena = new Arena(ctx, { isMarshalable: true });
121
+
122
+ class Cls {
123
+ hoge = "";
124
+
125
+ constructor(v = "foo") {
126
+ this.hoge = v;
127
+ }
128
+ }
129
+ arena.expose({ Cls: arena.sync(Cls) });
130
+
131
+ const instance = arena.evalCode(`new Cls("bar")`) as { hoge: string };
132
+ expect(instance.hoge).toBe("bar");
133
+ expect(arena.evalCode(`new Cls() instanceof Cls`)).toBe(true);
134
+
135
+ arena.dispose();
136
+ ctx.dispose();
137
+ });
138
+ });
139
+
76
140
  describe("evalCode", () => {
77
141
  test("simple object and function", async () => {
78
142
  const ctx = (await getQuickJS()).newContext();
@@ -254,6 +318,36 @@ describe("evalCode", () => {
254
318
  arena.dispose();
255
319
  ctx.dispose();
256
320
  });
321
+
322
+ test("options are forwarded (strict mode)", async () => {
323
+ const ctx = (await getQuickJS()).newContext();
324
+ const arena = new Arena(ctx, { isMarshalable: true });
325
+
326
+ // Assigning to an undeclared variable succeeds in sloppy mode...
327
+ expect(arena.evalCode("x = 1")).toBe(1);
328
+ // ...but throws a ReferenceError in strict mode.
329
+ expect(() => arena.evalCode("y = 1", undefined, { strict: true })).toThrow(ReferenceError);
330
+
331
+ arena.dispose();
332
+ ctx.dispose();
333
+ });
334
+
335
+ test("filename appears in error stacks", async () => {
336
+ const ctx = (await getQuickJS()).newContext();
337
+ const arena = new Arena(ctx, { isMarshalable: true });
338
+
339
+ try {
340
+ arena.evalCode(`throw new Error("boom")`, "my-special-file.js");
341
+ throw new Error("should have thrown");
342
+ } catch (e) {
343
+ expect(e).toBeInstanceOf(Error);
344
+ expect((e as Error).message).toBe("boom");
345
+ expect((e as Error).stack).toContain("my-special-file.js");
346
+ }
347
+
348
+ arena.dispose();
349
+ ctx.dispose();
350
+ });
257
351
  });
258
352
 
259
353
  describe("expose without sync", () => {
@@ -836,6 +930,30 @@ describe("evalModule", () => {
836
930
  arena.dispose();
837
931
  ctx.dispose();
838
932
  });
933
+
934
+ test("user options are merged and type is forced to module", async () => {
935
+ const ctx = (await getQuickJS()).newContext();
936
+ const arena = new Arena(ctx, { isMarshalable: true });
937
+
938
+ // `strict` is passed through, and a user-supplied `type` is overridden with
939
+ // "module" so `export` still works and the exports are returned.
940
+ const exports = arena.evalModule(
941
+ `
942
+ export const value = 42;
943
+ export function greet(name) {
944
+ return "Hi, " + name;
945
+ }
946
+ `,
947
+ "with-options.js",
948
+ { strict: true, type: "global" },
949
+ );
950
+
951
+ expect(exports.value).toBe(42);
952
+ expect(exports.greet("World")).toBe("Hi, World");
953
+
954
+ arena.dispose();
955
+ ctx.dispose();
956
+ });
839
957
  });
840
958
 
841
959
  describe("memory management", () => {
@@ -1035,6 +1153,27 @@ describe("memory management", () => {
1035
1153
  arena.dispose();
1036
1154
  ctx.dispose();
1037
1155
  });
1156
+
1157
+ test("setInterruptHandler interrupts infinite loops", async () => {
1158
+ const ctx = (await getQuickJS()).newContext();
1159
+ const arena = new Arena(ctx, { isMarshalable: true });
1160
+
1161
+ // Interrupt after the handler has been called a number of times.
1162
+ let calls = 0;
1163
+ arena.setInterruptHandler(() => ++calls > 1000);
1164
+
1165
+ expect(() => {
1166
+ arena.evalCode(`while (true) {}`);
1167
+ }).toThrow();
1168
+ expect(calls).toBeGreaterThan(1000);
1169
+
1170
+ // After removing the handler, normal evaluation works again.
1171
+ arena.removeInterruptHandler();
1172
+ expect(arena.evalCode(`1 + 2`)).toBe(3);
1173
+
1174
+ arena.dispose();
1175
+ ctx.dispose();
1176
+ });
1038
1177
  });
1039
1178
 
1040
1179
  describe("intrinsics configuration", () => {
@@ -1116,6 +1255,149 @@ describe("AsyncArena", () => {
1116
1255
  });
1117
1256
  });
1118
1257
 
1258
+ describe("asyncify (#32)", () => {
1259
+ test("without the option an async fn yields a Promise in the guest", async () => {
1260
+ const ctx = await newAsyncContext();
1261
+ const arena = new AsyncArena(ctx, { isMarshalable: true });
1262
+
1263
+ arena.expose({ f: async () => "result" });
1264
+ expect(await arena.evalCodeAsync(`typeof f()`)).toBe("object");
1265
+
1266
+ arena.dispose();
1267
+ ctx.dispose();
1268
+ });
1269
+
1270
+ test("asyncify: true resolves the value synchronously in the guest", async () => {
1271
+ const ctx = await newAsyncContext();
1272
+ const arena = new AsyncArena(ctx, { isMarshalable: true, asyncify: true });
1273
+
1274
+ arena.expose({
1275
+ f: async () => {
1276
+ await new Promise(r => setTimeout(r, 1));
1277
+ return "result";
1278
+ },
1279
+ g: async () => ({ a: 1, b: [2, 3] }),
1280
+ });
1281
+
1282
+ expect(await arena.evalCodeAsync(`typeof f()`)).toBe("string");
1283
+ expect(await arena.evalCodeAsync(`f()`)).toBe("result");
1284
+ expect(await arena.evalCodeAsync(`const o = g(); [typeof o, o.a, o.b[1]]`)).toEqual([
1285
+ "object",
1286
+ 1,
1287
+ 3,
1288
+ ]);
1289
+
1290
+ arena.dispose();
1291
+ ctx.dispose();
1292
+ });
1293
+
1294
+ test("asyncify: true passes arguments through", async () => {
1295
+ const ctx = await newAsyncContext();
1296
+ const arena = new AsyncArena(ctx, { isMarshalable: true, asyncify: true });
1297
+
1298
+ arena.expose({ add: async (a: number, b: number) => a + b });
1299
+ expect(await arena.evalCodeAsync(`add(2, 3)`)).toBe(5);
1300
+
1301
+ arena.dispose();
1302
+ ctx.dispose();
1303
+ });
1304
+
1305
+ test("predicate form only asyncifies matching functions", async () => {
1306
+ const ctx = await newAsyncContext();
1307
+ const arena = new AsyncArena(ctx, {
1308
+ isMarshalable: true,
1309
+ asyncify: target => (target as any).asyncified === true,
1310
+ });
1311
+
1312
+ const yes = async () => "sync-value";
1313
+ (yes as any).asyncified = true;
1314
+ const no = async () => "promise-value";
1315
+
1316
+ arena.expose({ yes, no });
1317
+
1318
+ expect(await arena.evalCodeAsync(`typeof yes()`)).toBe("string");
1319
+ expect(await arena.evalCodeAsync(`yes()`)).toBe("sync-value");
1320
+ expect(await arena.evalCodeAsync(`typeof no()`)).toBe("object");
1321
+
1322
+ arena.dispose();
1323
+ ctx.dispose();
1324
+ });
1325
+
1326
+ test("a rejecting asyncified fn surfaces as a catchable VM error", async () => {
1327
+ const ctx = await newAsyncContext();
1328
+ const arena = new AsyncArena(ctx, { isMarshalable: true, asyncify: true });
1329
+
1330
+ arena.expose({
1331
+ boom: async () => {
1332
+ throw new Error("kaboom");
1333
+ },
1334
+ });
1335
+
1336
+ // Uncaught in the guest: the error propagates back to the host.
1337
+ await expect(arena.evalCodeAsync(`boom()`)).rejects.toThrow("kaboom");
1338
+
1339
+ // Caught in the guest: the guest can handle it synchronously.
1340
+ expect(
1341
+ await arena.evalCodeAsync(`
1342
+ try {
1343
+ boom();
1344
+ "no-throw";
1345
+ } catch (e) {
1346
+ "caught:" + e.message;
1347
+ }
1348
+ `),
1349
+ ).toBe("caught:kaboom");
1350
+
1351
+ arena.dispose();
1352
+ ctx.dispose();
1353
+ });
1354
+
1355
+ test("sequential asyncified calls in one evalCodeAsync work", async () => {
1356
+ const ctx = await newAsyncContext();
1357
+ const arena = new AsyncArena(ctx, { isMarshalable: true, asyncify: true });
1358
+
1359
+ arena.expose({
1360
+ first: async () => 10,
1361
+ second: async (n: number) => n + 5,
1362
+ });
1363
+
1364
+ expect(await arena.evalCodeAsync(`const a = first(); const b = second(a); a + b`)).toBe(25);
1365
+
1366
+ arena.dispose();
1367
+ ctx.dispose();
1368
+ });
1369
+
1370
+ test("calling an asyncified fn during synchronous evalCode fails", async () => {
1371
+ const ctx = await newAsyncContext();
1372
+ const arena = new AsyncArena(ctx, { isMarshalable: true, asyncify: true });
1373
+
1374
+ arena.expose({ f: async () => "result" });
1375
+
1376
+ // Asyncify can only suspend through an async entry point; a synchronous
1377
+ // evalCode cannot unwind the stack, so it throws.
1378
+ expect(() => arena.evalCode(`f()`)).toThrow("Function unexpectedly returned a Promise");
1379
+
1380
+ arena.dispose();
1381
+ ctx.dispose();
1382
+ });
1383
+
1384
+ test("sync context fallback: asyncify does not break, async fn behaves as today", async () => {
1385
+ const ctx = (await getQuickJS()).newContext();
1386
+ const arena = new Arena(ctx, { isMarshalable: true, asyncify: true });
1387
+
1388
+ arena.expose({ f: async () => "result" });
1389
+ // No newAsyncifiedFunction on a plain context: falls back to Promise marshalling.
1390
+ expect(arena.evalCode(`globalThis.__p = f(); typeof __p`)).toBe("object");
1391
+ // Let the host promise settle into the VM promise before disposing, so the
1392
+ // marshalled resolution does not touch a disposed context.
1393
+ await new Promise(r => setTimeout(r));
1394
+ arena.executePendingJobs();
1395
+
1396
+ arena.dispose();
1397
+ ctx.dispose();
1398
+ });
1399
+ });
1400
+
1119
1401
  describe("Symbol.dispose", () => {
1120
1402
  test("using disposes the arena", async () => {
1121
1403
  const ctx = (await getQuickJS()).newContext();