From 4d91910701e52801de7668bba6d29742e63a283b Mon Sep 17 00:00:00 2001 From: Kasumi Hanazuki Date: Sat, 6 Jun 2026 04:59:10 +0000 Subject: [PATCH 1/3] refactor: move createVm to JsonnetWorker --- src/Jsonnet.cpp | 86 +++--------------------------------------- src/Jsonnet.hpp | 35 +---------------- src/JsonnetVmParam.hpp | 39 +++++++++++++++++++ src/JsonnetWorker.cpp | 70 +++++++++++++++++++++++++++++++++- src/JsonnetWorker.hpp | 5 ++- 5 files changed, 118 insertions(+), 117 deletions(-) create mode 100644 src/JsonnetVmParam.hpp diff --git a/src/Jsonnet.cpp b/src/Jsonnet.cpp index 8f366776..071e7ab3 100644 --- a/src/Jsonnet.cpp +++ b/src/Jsonnet.cpp @@ -1,15 +1,9 @@ // SPDX-License-Identifier: MIT #include "Jsonnet.hpp" -#include -#include #include -#include #include #include #include -#include "JsonValueConverter.hpp" -#include "JsonnetImportCallback.hpp" -#include "JsonnetNativeCallback.hpp" #include "JsonnetWorker.hpp" #include "libjsonnet.h" @@ -83,9 +77,8 @@ namespace nodejsonnet { auto const env = info.Env(); auto filename = info[0].As().Utf8Value(); - auto vm = createVm(env); auto const worker = new JsonnetWorker( - env, vm, std::make_unique(std::move(filename))); + env, *this, std::make_unique(std::move(filename))); auto const promise = worker->Promise(); worker->Queue(); return promise; @@ -96,8 +89,7 @@ namespace nodejsonnet { auto snippet = info[0].As().Utf8Value(); auto filename = info.Length() < 2 ? "(snippet)" : info[1].As().Utf8Value(); - auto vm = createVm(env); - auto const worker = new JsonnetWorker(env, vm, + auto const worker = new JsonnetWorker(env, *this, std::make_unique(std::move(snippet), std::move(filename))); auto const promise = worker->Promise(); worker->Queue(); @@ -108,9 +100,8 @@ namespace nodejsonnet { auto const env = info.Env(); auto filename = info[0].As().Utf8Value(); - auto vm = createVm(env); auto const worker = new JsonnetWorker( - env, vm, std::make_unique(std::move(filename))); + env, *this, std::make_unique(std::move(filename))); auto const promise = worker->Promise(); worker->Queue(); return promise; @@ -121,8 +112,7 @@ namespace nodejsonnet { auto snippet = info[0].As().Utf8Value(); auto filename = info.Length() < 2 ? "(snippet)" : info[1].As().Utf8Value(); - auto vm = createVm(env); - auto const worker = new JsonnetWorker(env, vm, + auto const worker = new JsonnetWorker(env, *this, std::make_unique( std::move(snippet), std::move(filename))); auto const promise = worker->Promise(); @@ -134,9 +124,8 @@ namespace nodejsonnet { auto const env = info.Env(); auto filename = info[0].As().Utf8Value(); - auto vm = createVm(env); auto const worker = new JsonnetWorker( - env, vm, std::make_unique(std::move(filename))); + env, *this, std::make_unique(std::move(filename))); auto const promise = worker->Promise(); worker->Queue(); return promise; @@ -147,8 +136,7 @@ namespace nodejsonnet { auto snippet = info[0].As().Utf8Value(); auto filename = info.Length() < 2 ? "(snippet)" : info[1].As().Utf8Value(); - auto vm = createVm(env); - auto const worker = new JsonnetWorker(env, vm, + auto const worker = new JsonnetWorker(env, *this, std::make_unique( std::move(snippet), std::move(filename))); auto const promise = worker->Promise(); @@ -211,66 +199,4 @@ namespace nodejsonnet { return info.This(); } - std::shared_ptr Jsonnet::createVm(Napi::Env const &env) { - auto vm = JsonnetVm::make(); - - if(maxStack) { - vm->maxStack(*maxStack); - } - if(maxTrace) { - vm->maxTrace(*maxTrace); - } - if(gcMinObjects) { - vm->gcMinObjects(*gcMinObjects); - } - if(gcGrowthTrigger) { - vm->gcGrowthTrigger(*gcGrowthTrigger); - } - vm->stringOutput(stringOutput); - vm->trailingNewline(trailingNewline); - - for(auto const &[name, var]: ext) { - if(var.isCode) { - vm->extCode(name, var.value); - } else { - vm->extVar(name, var.value); - } - } - - for(auto const &[name, var]: tla) { - if(var.isCode) { - vm->tlaCode(name, var.value); - } else { - vm->tlaVar(name, var.value); - } - } - - for(auto const &x: jpath) { - vm->jpathAdd(x); - } - - for(auto const &[name, cb]: nativeCallbacks) { - auto const &fun = cb.fun; - auto const ¶ms = cb.params; - - vm->addNativeCallback( - name, - [callback = std::make_shared(env, fun.Value())]( - std::shared_ptr vm, std::vector args) { - return callback->call(std::move(vm), std::move(args)); - }, - params); - } - - if(importCallbackParam) { - vm->setImportCallback( - [callback = std::make_shared(env, importCallbackParam->fun.Value())]( - std::shared_ptr vm, std::string const &base, std::string const &rel) { - return callback->call(std::move(vm), base, rel); - }); - } - - return vm; - } - } diff --git a/src/Jsonnet.hpp b/src/Jsonnet.hpp index ceeb22cd..fa165c4f 100644 --- a/src/Jsonnet.hpp +++ b/src/Jsonnet.hpp @@ -1,42 +1,11 @@ // SPDX-License-Identifier: MIT #pragma once -#include -#include -#include -#include #include -#include "JsonnetVm.hpp" +#include "JsonnetVmParam.hpp" namespace nodejsonnet { - struct JsonnetVmParam { - struct Variable { - bool isCode; - std::string value; - }; - - struct NativeCallbackParam { - Napi::FunctionReference fun; - std::vector params; - }; - - struct ImportCallbackParam { - Napi::FunctionReference fun; - }; - - std::optional maxStack, maxTrace; - std::optional gcMinObjects; - std::optional gcGrowthTrigger; - bool stringOutput = false; - bool trailingNewline = true; - - std::map ext, tla; - std::vector jpath; - std::map nativeCallbacks; - std::optional importCallbackParam; - }; - class Jsonnet: public Napi::ObjectWrap, private JsonnetVmParam { public: static Napi::Function init(const Napi::Env env); @@ -65,8 +34,6 @@ namespace nodejsonnet { Napi::Value addJpath(const Napi::CallbackInfo &info); Napi::Value nativeCallback(const Napi::CallbackInfo &info); Napi::Value importCallback(const Napi::CallbackInfo &info); - - std::shared_ptr createVm(Napi::Env const &env); }; } diff --git a/src/JsonnetVmParam.hpp b/src/JsonnetVmParam.hpp new file mode 100644 index 00000000..087db18c --- /dev/null +++ b/src/JsonnetVmParam.hpp @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: MIT +#pragma once + +#include +#include +#include +#include +#include + +namespace nodejsonnet { + + struct JsonnetVmParam { + struct Variable { + bool isCode; + std::string value; + }; + + struct NativeCallbackParam { + Napi::FunctionReference fun; + std::vector params; + }; + + struct ImportCallbackParam { + Napi::FunctionReference fun; + }; + + std::optional maxStack, maxTrace; + std::optional gcMinObjects; + std::optional gcGrowthTrigger; + bool stringOutput = false; + bool trailingNewline = true; + + std::map ext, tla; + std::vector jpath; + std::map nativeCallbacks; + std::optional importCallbackParam; + }; + +} diff --git a/src/JsonnetWorker.cpp b/src/JsonnetWorker.cpp index d0f8a695..c6aa6e59 100644 --- a/src/JsonnetWorker.cpp +++ b/src/JsonnetWorker.cpp @@ -5,14 +5,80 @@ #include #include #include "JsonnetAddon.hpp" +#include "JsonnetImportCallback.hpp" +#include "JsonnetNativeCallback.hpp" namespace nodejsonnet { - JsonnetWorker::JsonnetWorker(Napi::Env env, std::shared_ptr vm, std::unique_ptr op) - : Napi::AsyncWorker(env, "jsonnet"), vm(std::move(vm)), op(std::move(op)), + JsonnetWorker::JsonnetWorker(Napi::Env env, JsonnetVmParam const ¶m, std::unique_ptr op) + : Napi::AsyncWorker(env, "jsonnet"), vm(createVm(env, param)), op(std::move(op)), deferred(Napi::Promise::Deferred::New(env)) { } + std::shared_ptr JsonnetWorker::createVm( + Napi::Env const &env, JsonnetVmParam const ¶m) { + auto vm = JsonnetVm::make(); + + if(param.maxStack) { + vm->maxStack(*param.maxStack); + } + if(param.maxTrace) { + vm->maxTrace(*param.maxTrace); + } + if(param.gcMinObjects) { + vm->gcMinObjects(*param.gcMinObjects); + } + if(param.gcGrowthTrigger) { + vm->gcGrowthTrigger(*param.gcGrowthTrigger); + } + vm->stringOutput(param.stringOutput); + vm->trailingNewline(param.trailingNewline); + + for(auto const &[name, var]: param.ext) { + if(var.isCode) { + vm->extCode(name, var.value); + } else { + vm->extVar(name, var.value); + } + } + + for(auto const &[name, var]: param.tla) { + if(var.isCode) { + vm->tlaCode(name, var.value); + } else { + vm->tlaVar(name, var.value); + } + } + + for(auto const &x: param.jpath) { + vm->jpathAdd(x); + } + + for(auto const &[name, cb]: param.nativeCallbacks) { + auto const &fun = cb.fun; + auto const ¶ms = cb.params; + + vm->addNativeCallback( + name, + [callback = std::make_shared(env, fun.Value())]( + std::shared_ptr vm, std::vector args) { + return callback->call(std::move(vm), std::move(args)); + }, + params); + } + + if(param.importCallbackParam) { + vm->setImportCallback( + [callback = std::make_shared( + env, param.importCallbackParam->fun.Value())]( + std::shared_ptr vm, std::string const &base, std::string const &rel) { + return callback->call(std::move(vm), base, rel); + }); + } + + return vm; + } + void JsonnetWorker::Execute() { try { result = op->execute(*vm); diff --git a/src/JsonnetWorker.hpp b/src/JsonnetWorker.hpp index 8156f0c9..6f20be18 100644 --- a/src/JsonnetWorker.hpp +++ b/src/JsonnetWorker.hpp @@ -5,6 +5,7 @@ #include #include #include "JsonnetVm.hpp" +#include "JsonnetVmParam.hpp" namespace nodejsonnet { @@ -64,7 +65,7 @@ namespace nodejsonnet { Jsonnet, }; - JsonnetWorker(Napi::Env env, std::shared_ptr vm, std::unique_ptr op); + JsonnetWorker(Napi::Env env, JsonnetVmParam const ¶m, std::unique_ptr op); Napi::Promise Promise() { return deferred.Promise(); @@ -76,6 +77,8 @@ namespace nodejsonnet { void OnError(Napi::Error const &error) override; private: + static std::shared_ptr createVm(Napi::Env const &env, JsonnetVmParam const ¶m); + std::shared_ptr vm; std::unique_ptr op; Napi::Promise::Deferred deferred; From 4f86372041cfa8aeedb878151735ab4b84b6a0c8 Mon Sep 17 00:00:00 2001 From: Kasumi Hanazuki Date: Sat, 6 Jun 2026 06:32:14 +0000 Subject: [PATCH 2/3] refactor: extract common logic from Jsonnet::evalute* methods --- src/Jsonnet.cpp | 62 ++++++++++++++----------------------------------- src/Jsonnet.hpp | 4 ++++ 2 files changed, 22 insertions(+), 44 deletions(-) diff --git a/src/Jsonnet.cpp b/src/Jsonnet.cpp index 071e7ab3..eaf7b949 100644 --- a/src/Jsonnet.cpp +++ b/src/Jsonnet.cpp @@ -4,7 +4,6 @@ #include #include #include -#include "JsonnetWorker.hpp" #include "libjsonnet.h" namespace nodejsonnet { @@ -73,75 +72,50 @@ namespace nodejsonnet { return info.This(); } - Napi::Value Jsonnet::evaluateFile(const Napi::CallbackInfo &info) { - auto const env = info.Env(); - auto filename = info[0].As().Utf8Value(); - - auto const worker = new JsonnetWorker( - env, *this, std::make_unique(std::move(filename))); + Napi::Value Jsonnet::evaluate(Napi::Env const &env, std::unique_ptr op) { + auto const worker = new JsonnetWorker(env, *this, std::move(op)); auto const promise = worker->Promise(); - worker->Queue(); + worker->Queue(); // worker is deleted when it is done return promise; } + Napi::Value Jsonnet::evaluateFile(const Napi::CallbackInfo &info) { + auto filename = info[0].As().Utf8Value(); + return evaluate( + info.Env(), std::make_unique(std::move(filename))); + } + Napi::Value Jsonnet::evaluateSnippet(const Napi::CallbackInfo &info) { - auto const env = info.Env(); auto snippet = info[0].As().Utf8Value(); auto filename = info.Length() < 2 ? "(snippet)" : info[1].As().Utf8Value(); - - auto const worker = new JsonnetWorker(env, *this, + return evaluate(info.Env(), std::make_unique(std::move(snippet), std::move(filename))); - auto const promise = worker->Promise(); - worker->Queue(); - return promise; } Napi::Value Jsonnet::evaluateFileMulti(const Napi::CallbackInfo &info) { - auto const env = info.Env(); auto filename = info[0].As().Utf8Value(); - - auto const worker = new JsonnetWorker( - env, *this, std::make_unique(std::move(filename))); - auto const promise = worker->Promise(); - worker->Queue(); - return promise; + return evaluate( + info.Env(), std::make_unique(std::move(filename))); } Napi::Value Jsonnet::evaluateSnippetMulti(const Napi::CallbackInfo &info) { - auto const env = info.Env(); auto snippet = info[0].As().Utf8Value(); auto filename = info.Length() < 2 ? "(snippet)" : info[1].As().Utf8Value(); - - auto const worker = new JsonnetWorker(env, *this, - std::make_unique( - std::move(snippet), std::move(filename))); - auto const promise = worker->Promise(); - worker->Queue(); - return promise; + return evaluate(info.Env(), std::make_unique( + std::move(snippet), std::move(filename))); } Napi::Value Jsonnet::evaluateFileStream(const Napi::CallbackInfo &info) { - auto const env = info.Env(); auto filename = info[0].As().Utf8Value(); - - auto const worker = new JsonnetWorker( - env, *this, std::make_unique(std::move(filename))); - auto const promise = worker->Promise(); - worker->Queue(); - return promise; + return evaluate( + info.Env(), std::make_unique(std::move(filename))); } Napi::Value Jsonnet::evaluateSnippetStream(const Napi::CallbackInfo &info) { - auto const env = info.Env(); auto snippet = info[0].As().Utf8Value(); auto filename = info.Length() < 2 ? "(snippet)" : info[1].As().Utf8Value(); - - auto const worker = new JsonnetWorker(env, *this, - std::make_unique( - std::move(snippet), std::move(filename))); - auto const promise = worker->Promise(); - worker->Queue(); - return promise; + return evaluate(info.Env(), std::make_unique( + std::move(snippet), std::move(filename))); } Napi::Value Jsonnet::extString(const Napi::CallbackInfo &info) { diff --git a/src/Jsonnet.hpp b/src/Jsonnet.hpp index fa165c4f..ecc011bb 100644 --- a/src/Jsonnet.hpp +++ b/src/Jsonnet.hpp @@ -1,8 +1,10 @@ // SPDX-License-Identifier: MIT #pragma once +#include #include #include "JsonnetVmParam.hpp" +#include "JsonnetWorker.hpp" namespace nodejsonnet { @@ -34,6 +36,8 @@ namespace nodejsonnet { Napi::Value addJpath(const Napi::CallbackInfo &info); Napi::Value nativeCallback(const Napi::CallbackInfo &info); Napi::Value importCallback(const Napi::CallbackInfo &info); + + Napi::Value evaluate(Napi::Env const &env, std::unique_ptr op); }; } From 18c361145e422ca8bf3c7484f582e1b9244628d5 Mon Sep 17 00:00:00 2001 From: Kasumi Hanazuki Date: Sat, 6 Jun 2026 14:53:14 +0000 Subject: [PATCH 3/3] return original error from callbacks as JsonnetError.cause --- CHANGELOG.md | 1 + package-lock.json | 2 +- package.json | 2 +- spec/binding_spec.cjs | 49 ++++++++++++++++++++++++++++++------------- src/Callback.cpp | 12 +++++++++++ src/Callback.hpp | 20 +++++++++++------- src/JsonnetWorker.cpp | 35 ++++++++++++++++++++++--------- src/JsonnetWorker.hpp | 3 ++- types/tsconfig.json | 2 +- 9 files changed, 90 insertions(+), 36 deletions(-) create mode 100644 src/Callback.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 07aa076e..51c8293f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## UNRELEASED +- When a `nativeCallback` or `importCallback` function throws (synchronously or asynchronously), the `JsonnetError` rejection now carries the original JavaScript error as its `cause` property. - Cyclic references in values returned from a native callback are now detected and reported as an exception, instead of crashing the VM with a stack overflow. - Native callbacks now honor `toJSON()` on returned values; values that are not serializable (functions, symbols) are omitted from objects or serialized as `null` in arrays, consistent with `JSON.stringify` semantics. - Fix: `evaluateSnippetMulti` and `evaluateFileMulti` now correctly preserve a file key named `__proto__` in the returned object (no security impact: the key was silently dropped rather than causing prototype pollution). diff --git a/package-lock.json b/package-lock.json index 59c0e06d..40e53b81 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "devDependencies": { "@types/node": "20", "glob": "^13.0.0", - "jasmine": "^6.0.0", + "jasmine": "^6.2.0", "tstyche": "^7.0.0", "tsx": "^4.22.3", "typedoc": "^0.28.1", diff --git a/package.json b/package.json index 9ca4cd19..4a1af61f 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "devDependencies": { "@types/node": "20", "glob": "^13.0.0", - "jasmine": "^6.0.0", + "jasmine": "^6.2.0", "tsx": "^4.22.3", "tstyche": "^7.0.0", "typedoc": "^0.28.1", diff --git a/spec/binding_spec.cjs b/spec/binding_spec.cjs index efb0ba10..3d3a7197 100644 --- a/spec/binding_spec.cjs +++ b/spec/binding_spec.cjs @@ -351,18 +351,18 @@ describe('binding', () => { it('propagates rejection from thenable returned by native callback', async () => { const jsonnet = new Jsonnet(); - jsonnet.nativeCallback("fail", (msg) => ({ then: (_, reject) => reject(msg) }), "msg"); + jsonnet.nativeCallback("fail", (msg) => ({ then: (_, reject) => reject(new Error(msg)) }), "msg"); await expectAsync(jsonnet.evaluateSnippet(`std.native("fail")("kimagure")`)) - .toBeRejectedWithError(JsonnetError, /^RUNTIME ERROR: kimagure/); + .toBeRejectedWithError(JsonnetError, /^RUNTIME ERROR:.* kimagure/); }); it('propagates synchronous throw from .then() on promise returned by native callback', async () => { const jsonnet = new Jsonnet(); - jsonnet.nativeCallback("fail", () => ({ then: () => { throw "then threw"; } })); + jsonnet.nativeCallback("fail", () => ({ then: () => { throw new Error("then threw"); } })); await expectAsync(jsonnet.evaluateSnippet(`std.native("fail")()`)) - .toBeRejectedWithError(JsonnetError, /^RUNTIME ERROR: then threw/); + .toBeRejectedWithError(JsonnetError, /^RUNTIME ERROR:.* then threw/); }); it('uses the native callback added most recently for the same name', async () => { @@ -389,10 +389,15 @@ describe('binding', () => { it('reports throwing native callback', async () => { const jsonnet = new Jsonnet(); - jsonnet.nativeCallback("fail", (msg) => { throw msg; }, "msg"); + jsonnet.nativeCallback("fail", (msg) => { throw new TypeError(msg); }, "msg"); await expectAsync(jsonnet.evaluateSnippet(`std.native("fail")("kimagure")`)) - .toBeRejectedWithError(JsonnetError, /^RUNTIME ERROR: kimagure/); - + .toBeRejectedWithMatching(err => { + expect(err).toBeInstanceOf(JsonnetError); + expect(err.message).toMatch(/^RUNTIME ERROR:.* kimagure/); + expect(err.cause).toBeInstanceOf(TypeError); + expect(err.cause.message).toEqual("kimagure"); + return true; + }); }); it('propagates error when native callback result object has a throwing ownKeys trap', async () => { @@ -430,9 +435,15 @@ describe('binding', () => { it('reports throwing async native callback', async () => { const jsonnet = new Jsonnet(); - jsonnet.nativeCallback("failAsync", async (msg) => { throw msg; }, "msg"); + jsonnet.nativeCallback("failAsync", async (msg) => { throw new Error(msg); }, "msg"); await expectAsync(jsonnet.evaluateSnippet(`std.native("failAsync")("kimagure")`)) - .toBeRejectedWithError(JsonnetError, /^RUNTIME ERROR: kimagure/); + .toBeRejectedWithMatching(err => { + expect(err).toBeInstanceOf(JsonnetError); + expect(err.message).toMatch(/^RUNTIME ERROR:.* kimagure/); + expect(err.cause).toBeInstanceOf(Error); + expect(err.cause.message).toEqual("kimagure"); + return true; + }); }); it('reports syntax error in snippet with filename', async () => { @@ -658,7 +669,7 @@ describe('binding', () => { it('propagates synchronous throw from .then() on promise returned by import callback', async () => { const jsonnet = new Jsonnet() - .importCallback(() => ({ then: () => { throw "then threw"; } })); + .importCallback(() => ({ then: () => { throw new Error("then threw"); } })); await expectAsync(jsonnet.evaluateSnippet('import "x.jsonnet"')) .toBeRejectedWithError(JsonnetError, /then threw/); }); @@ -695,16 +706,26 @@ describe('binding', () => { const jsonnet = new Jsonnet() .importCallback((base, rel) => { throw new Error(`missing: ${rel}`); }); await expectAsync(jsonnet.evaluateSnippet('import "x.jsonnet"')) - .toBeRejectedWithError(JsonnetError, - /missing: x\.jsonnet/); + .toBeRejectedWithMatching(err => { + expect(err).toBeInstanceOf(JsonnetError); + expect(err.message).toMatch(/missing: x\.jsonnet/); + expect(err.cause).toBeInstanceOf(Error); + expect(err.cause.message).toEqual('missing: x.jsonnet'); + return true; + }); }); it('propagates async rejection as JsonnetError', async () => { const jsonnet = new Jsonnet() .importCallback(async (base, rel) => { throw new Error(`missing: ${rel}`); }); await expectAsync(jsonnet.evaluateSnippet('import "x.jsonnet"')) - .toBeRejectedWithError(JsonnetError, - /missing: x\.jsonnet/); + .toBeRejectedWithMatching(err => { + expect(err).toBeInstanceOf(JsonnetError); + expect(err.message).toMatch(/missing: x\.jsonnet/); + expect(err.cause).toBeInstanceOf(Error); + expect(err.cause.message).toEqual('missing: x.jsonnet'); + return true; + }); }); it('takes precedence over addJpath', async () => { diff --git a/src/Callback.cpp b/src/Callback.cpp new file mode 100644 index 00000000..624d922e --- /dev/null +++ b/src/Callback.cpp @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +#include "Callback.hpp" + +namespace nodejsonnet { + + CallbackError::CallbackError(std::string const &resourceName, Napi::Error e) + : std::runtime_error{std::string("JavaScript exception throw in ") + resourceName + ": " + + e.Message()}, + jsError{std::make_shared(e)} { + } + +} diff --git a/src/Callback.hpp b/src/Callback.hpp index 89466128..6e02b559 100644 --- a/src/Callback.hpp +++ b/src/Callback.hpp @@ -11,6 +11,12 @@ namespace nodejsonnet { + // Wrap Napi::Error in plain C++ error so it can safely pass throgh non-JS thread + struct CallbackError: std::runtime_error { + explicit CallbackError(std::string const &resourceName, Napi::Error e); + std::shared_ptr jsError; + }; + template struct CallbackPayload { explicit CallbackPayload(std::shared_ptr vm): vm{std::move(vm)} { } @@ -74,12 +80,12 @@ namespace nodejsonnet { auto const on_success = Napi::Function::New( env, - [](Napi::CallbackInfo const &info) { + [](Napi::CallbackInfo const &info) noexcept { auto const p = static_cast(info.Data()); try { p->resolveResult(info[0]); } catch(Napi::Error const &e) { - p->setError(std::make_exception_ptr(std::runtime_error(e.Message()))); + p->setError(std::make_exception_ptr(CallbackError{PayloadType::resourceName, e})); } catch(...) { p->setError(std::current_exception()); } @@ -88,13 +94,11 @@ namespace nodejsonnet { auto const on_failure = Napi::Function::New( env, - [](Napi::CallbackInfo const &info) { + [](Napi::CallbackInfo const &info) noexcept { auto const p = static_cast(info.Data()); try { - auto const error = info[0].ToString(); - p->setError(std::make_exception_ptr(std::runtime_error(error))); - } catch(Napi::Error const &e) { - p->setError(std::make_exception_ptr(std::runtime_error(e.Message()))); + p->setError(std::make_exception_ptr( + CallbackError{PayloadType::resourceName, Napi::Error(info.Env(), info[0])})); } catch(...) { p->setError(std::current_exception()); } @@ -103,7 +107,7 @@ namespace nodejsonnet { result.template As().Then(on_success, on_failure); } catch(Napi::Error const &e) { - payload->setError(std::make_exception_ptr(std::runtime_error(e.Message()))); + payload->setError(std::make_exception_ptr(CallbackError{PayloadType::resourceName, e})); } catch(...) { payload->setError(std::current_exception()); } diff --git a/src/JsonnetWorker.cpp b/src/JsonnetWorker.cpp index c6aa6e59..80119032 100644 --- a/src/JsonnetWorker.cpp +++ b/src/JsonnetWorker.cpp @@ -60,19 +60,29 @@ namespace nodejsonnet { vm->addNativeCallback( name, - [callback = std::make_shared(env, fun.Value())]( + [this, callback = std::make_shared(env, fun.Value())]( std::shared_ptr vm, std::vector args) { - return callback->call(std::move(vm), std::move(args)); + try { + return callback->call(std::move(vm), std::move(args)); + } catch(CallbackError &e) { + this->jsError = std::move(e.jsError); + throw; + } }, params); } if(param.importCallbackParam) { vm->setImportCallback( - [callback = std::make_shared( - env, param.importCallbackParam->fun.Value())]( + [this, callback = std::make_shared( + env, param.importCallbackParam->fun.Value())]( std::shared_ptr vm, std::string const &base, std::string const &rel) { - return callback->call(std::move(vm), base, rel); + try { + return callback->call(std::move(vm), base, rel); + } catch(CallbackError &e) { + this->jsError = std::move(e.jsError); + throw; + } }); } @@ -98,12 +108,17 @@ namespace nodejsonnet { switch(errorType) { case ErrorType::Generic: break; - case ErrorType::Jsonnet: - e = JsonnetAddon::getInstance(e.Env()) - .getExport("JsonnetError") - .As() - .New({e.Get("message")}); + case ErrorType::Jsonnet: { + auto const env = e.Env(); + auto const ctor = + JsonnetAddon::getInstance(env).getExport("JsonnetError").As(); + auto options = Napi::Object::New(env); + if(jsError) { + options.Set("cause", jsError->Value()); + } + e = ctor.New({e.Get("message"), options}); break; + } default: abort(); // unreachable } diff --git a/src/JsonnetWorker.hpp b/src/JsonnetWorker.hpp index 6f20be18..f065d05d 100644 --- a/src/JsonnetWorker.hpp +++ b/src/JsonnetWorker.hpp @@ -77,13 +77,14 @@ namespace nodejsonnet { void OnError(Napi::Error const &error) override; private: - static std::shared_ptr createVm(Napi::Env const &env, JsonnetVmParam const ¶m); + std::shared_ptr createVm(Napi::Env const &env, JsonnetVmParam const ¶m); std::shared_ptr vm; std::unique_ptr op; Napi::Promise::Deferred deferred; std::optional result; ErrorType errorType = ErrorType::Generic; + std::shared_ptr jsError; }; } diff --git a/types/tsconfig.json b/types/tsconfig.json index 8eab74bc..575cceb0 100644 --- a/types/tsconfig.json +++ b/types/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "module": "commonjs", - "lib": ["es6"], + "lib": ["es2022"], "strict": true, "types": ["node"], "paths": { "@hanazuki/node-jsonnet": ["."] }