Skip to content

Commit f07790d

Browse files
authored
evalCode(type=module) throws instead of returning rejected promise (bellard#161)
1 parent da55055 commit f07790d

File tree

3 files changed

+72
-18
lines changed

3 files changed

+72
-18
lines changed

c/interface.c

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -736,9 +736,12 @@ MaybeAsync(JSValue *) QTS_Eval(JSContext *ctx, BorrowedHeapChar *js_code, size_t
736736
JS_FreeValue(ctx, eval_result);
737737
return jsvalue_to_heap(JS_GetModuleNamespace(ctx, module));
738738
} else if (state == JS_PROMISE_REJECTED) {
739-
QTS_DEBUG("QTS_Eval: result: JS_PROMISE_REJECTED")
740-
// Module failed, just return the rejected result.
741-
return jsvalue_to_heap(eval_result);
739+
// Throw the rejection response, to match evaluation of non-module code,
740+
// or of code with a syntax error.
741+
QTS_DEBUG("QTS_Eval: result: JS_PROMISE_REJECTED - throw rejection reason")
742+
JS_Throw(ctx, JS_PromiseResult(ctx, eval_result));
743+
JS_FreeValue(ctx, eval_result);
744+
return jsvalue_to_heap(JS_EXCEPTION);
742745
} else if (state == JS_PROMISE_PENDING) {
743746
QTS_DEBUG("QTS_Eval: result: JS_PROMISE_PENDING")
744747
// return moduleDone.then(() => module_namespace)
@@ -783,7 +786,6 @@ JSValue *QTS_GetModuleNamespace(JSContext *ctx, JSValueConst *module_func_obj) {
783786

784787
OwnedHeapChar *QTS_Typeof(JSContext *ctx, JSValueConst *value) {
785788
const char *result = "unknown";
786-
uint32_t tag = JS_VALUE_GET_TAG(*value);
787789

788790
if (JS_IsNumber(*value)) {
789791
result = "number";

packages/quickjs-emscripten-core/src/context.ts

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
1-
import {
2-
type EitherModule,
3-
type EvalDetectModule,
4-
type EvalFlags,
5-
type JSBorrowedCharPointer,
6-
type JSContextPointer,
7-
type JSRuntimePointer,
8-
type JSValueConstPointer,
9-
type JSValuePointer,
10-
type JSValuePointerPointer,
11-
type EitherFFI,
12-
JSPromiseStateEnum,
1+
import { JSPromiseStateEnum } from "@jitl/quickjs-ffi-types"
2+
import type {
3+
EvalFlags,
4+
EitherModule,
5+
EvalDetectModule,
6+
JSBorrowedCharPointer,
7+
JSContextPointer,
8+
JSRuntimePointer,
9+
JSValueConstPointer,
10+
JSValuePointer,
11+
JSValuePointerPointer,
12+
EitherFFI,
1313
} from "@jitl/quickjs-ffi-types"
1414
import { debugLog } from "./debug"
1515
import type { JSPromiseState } from "./deferred-promise"
@@ -966,9 +966,10 @@ export class QuickJSContext
966966

967967
/**
968968
* Dump a JSValue to Javascript in a best-effort fashion.
969+
* If the value is a promise, dumps the promise's state.
969970
* Returns `handle.toString()` if it cannot be serialized to JSON.
970971
*/
971-
dump(handle: QuickJSHandle) {
972+
dump(handle: QuickJSHandle): any {
972973
this.runtime.assertOwned(handle)
973974
const type = this.typeof(handle)
974975
if (type === "string") {
@@ -983,6 +984,20 @@ export class QuickJSContext
983984
return this.getSymbol(handle)
984985
}
985986

987+
// It's confusing if we dump(promise) and just get back {} because promise
988+
// has no properties, so dump promise state.
989+
const asPromiseState = this.getPromiseState(handle)
990+
if (asPromiseState.type === "fulfilled" && !asPromiseState.notAPromise) {
991+
handle.dispose()
992+
return { type: asPromiseState.type, value: asPromiseState.value.consume(this.dump) }
993+
} else if (asPromiseState.type === "pending") {
994+
handle.dispose()
995+
return { type: asPromiseState.type }
996+
} else if (asPromiseState.type === "rejected") {
997+
handle.dispose()
998+
return { type: asPromiseState.type, error: asPromiseState.error.consume(this.dump) }
999+
}
1000+
9861001
const str = this.memory.consumeJSCharPointer(this.ffi.QTS_Dump(this.ctx.value, handle.value))
9871002
try {
9881003
return JSON.parse(str)

packages/quickjs-emscripten/src/quickjs.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import assert from "assert"
66
import fs from "fs"
7+
import { inspect as baseInspect } from "node:util"
78
import { describe, beforeEach, afterEach, afterAll as after, it, expect } from "vitest"
89
import type {
910
QuickJSHandle,
@@ -401,7 +402,7 @@ function contextTests(getContext: GetTestContext, isDebug = false) {
401402
value.dispose()
402403
})
403404

404-
it("on failure: returns { error: exception }", () => {
405+
it("on syntax error: returns { error: exception }", () => {
405406
const result = vm.evalCode(`["this", "should", "fail].join(' ')`)
406407
if (!result.error) {
407408
assert.fail("result should be an error")
@@ -413,6 +414,35 @@ function contextTests(getContext: GetTestContext, isDebug = false) {
413414
result.error.dispose()
414415
})
415416

417+
it("module: on syntax error: returns { error: exception }", () => {
418+
const result = vm.evalCode(`["this", "should", "fail].join(' ')`, "mod.js", {
419+
type: "module",
420+
})
421+
if (!result.error) {
422+
assert.fail("result should be an error")
423+
}
424+
assertError(vm.dump(result.error), {
425+
name: "SyntaxError",
426+
message: "unexpected end of string",
427+
})
428+
result.error.dispose()
429+
})
430+
431+
it("module: on TypeError: returns { error: exception }", () => {
432+
const result = vm.evalCode(`null.prop`, "mod.js", { type: "module" })
433+
if (!result.error) {
434+
result.value.consume((val) =>
435+
assert.fail(`result should be an error, instead is: ${inspect(vm.dump(val))}`),
436+
)
437+
return
438+
}
439+
const error = result.error.consume(vm.dump)
440+
assertError(error, {
441+
name: "TypeError",
442+
message: "cannot read property 'prop' of null",
443+
})
444+
})
445+
416446
it("runs in the global context", () => {
417447
vm.unwrapResult(vm.evalCode(`var declaredWithEval = 'Nice!'`)).dispose()
418448
const declaredWithEval = vm.getProp(vm.global, "declaredWithEval")
@@ -1377,3 +1407,10 @@ applyVitestHack(QuickJSWASMModule, () => ({
13771407
type: "QuickJSWASMModule",
13781408
__vitest__: true,
13791409
}))
1410+
1411+
const inspect = (val: unknown) => {
1412+
return baseInspect(val, {
1413+
depth: 30,
1414+
colors: true,
1415+
})
1416+
}

0 commit comments

Comments
 (0)