diff --git a/c/interface.c b/c/interface.c index 4546fc58f..da9d6c85f 100644 --- a/c/interface.c +++ b/c/interface.c @@ -1098,8 +1098,10 @@ MaybeAsync(JSValue *) QTS_Eval(JSContext *ctx, BorrowedHeapChar *js_code, size_t // Error - nothing more to do. JS_IsException(eval_result) // Non-module eval - return the result - || (evalFlags & JS_EVAL_TYPE_MODULE) == 0) { - QTS_DEBUG("QTS_Eval: non-module or exception") + || (evalFlags & JS_EVAL_TYPE_MODULE) == 0 + // Compiled code has not been evaluated; return it without accessing module. + || (evalFlags & JS_EVAL_FLAG_COMPILE_ONLY) != 0) { + QTS_DEBUG("QTS_Eval: non-module, compile-only, or exception") return jsvalue_to_heap(eval_result); } @@ -1114,7 +1116,6 @@ MaybeAsync(JSValue *) QTS_Eval(JSContext *ctx, BorrowedHeapChar *js_code, size_t // quickjs@2024-01-14 evaluating module // produced a promise (state == JS_PROMISE_FULFILLED) - // quickjs in compile mode // quickjs-ng before rebasing on quickjs@2024-01-14 // not a promise. || (state == -1)) { diff --git a/packages/quickjs-emscripten/src/quickjs.test.ts b/packages/quickjs-emscripten/src/quickjs.test.ts index f603c256c..1cfcb6c4a 100644 --- a/packages/quickjs-emscripten/src/quickjs.test.ts +++ b/packages/quickjs-emscripten/src/quickjs.test.ts @@ -541,6 +541,34 @@ function contextTests(getContext: GetTestContext, options: ContextTestOptions = }) describe(".evalCode", () => { + describe("compileOnly", () => { + for (const type of ["global", "module"] as const) { + it(`${type}: returns disposable compiled code without executing it`, () => { + vm.unwrapResult(vm.evalCode("globalThis.compileOnlyRuns = 0")).dispose() + const code = + type === "module" + ? "globalThis.compileOnlyRuns++; export const value = 42" + : "globalThis.compileOnlyRuns++; 42" + const compiled = vm.unwrapResult( + vm.evalCode(code, "compiled.js", { type, compileOnly: true }), + ) + // Compiled code is bytecode, not a JS value, so it has no JS type. + assert.equal(vm.typeof(compiled), "unknown") + compiled.dispose() + assert.equal(vm.getProp(vm.global, "compileOnlyRuns").consume(vm.getNumber), 0) + }) + } + + it("module: preserves syntax errors", () => { + const result = vm.evalCode("export const =", "invalid.js", { + type: "module", + compileOnly: true, + }) + assert(result.error, "Compilation should return a syntax error") + assert.equal(result.error.consume(vm.dump).name, "SyntaxError") + }) + }) + it("on success: returns { value: success }", () => { const value = vm.unwrapResult(vm.evalCode(`["this", "should", "work"].join(' ')`)) assert.equal(vm.getString(value), "this should work") @@ -1268,6 +1296,29 @@ function asyncContextTests( }) describe("module loading", () => { + it("compileOnly: returns disposable compiled code without executing it", async () => { + vm.unwrapResult(vm.evalCode("globalThis.compileOnlyRuns = 0")).dispose() + const result = await vm.evalCodeAsync( + "globalThis.compileOnlyRuns++; export const value = 42", + "compiled.js", + { type: "module", compileOnly: true }, + ) + const compiled = vm.unwrapResult(result) + // Compiled code is bytecode, not a JS value, so it has no JS type. + assert.equal(vm.typeof(compiled), "unknown") + compiled.dispose() + assert.equal(vm.getProp(vm.global, "compileOnlyRuns").consume(vm.getNumber), 0) + }) + + it("compileOnly: preserves syntax errors", async () => { + const result = await vm.evalCodeAsync("export const =", "invalid.js", { + type: "module", + compileOnly: true, + }) + assert(result.error, "Compilation should return a syntax error") + assert.equal(result.error.consume(vm.dump).name, "SyntaxError") + }) + it("supports async module loading", async () => { async function testBody() { let moduleLoaderCalls = 0