difftreelog
refactor(web) idiomatic js api
in: master
10 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1119,6 +1119,7 @@
"jrsonnet-stdlib",
"jrsonnet-types",
"js-sys",
+ "rustc-hash 2.1.2",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
README.adocdiffbeforeafterboth--- a/README.adoc
+++ b/README.adoc
@@ -88,6 +88,21 @@
Jrsonnet is written in rust itself, so just add it as dependency
+=== JavaScript/TypeScript (WASM)
+
+// image:https://img.shields.io/npm/v/jrsonnet[alt=npm, link=https://www.npmjs.com/package/jrsonnet]
+image:https://jsr.io/badges/@jrsonnet/jrsonnet[alt=JSR, link=https://jsr.io/@jrsonnet/jrsonnet]
+image:https://jsr.io/badges/@jrsonnet/jrsonnet/score[alt=JSR score, link=https://jsr.io/@jrsonnet/jrsonnet]
+
+WASM bindings are published to JSR as `@jrsonnet/jrsonnet`.
+
+[source,sh]
+----
+deno add jsr:@jrsonnet/jrsonnet
+----
+
+// npm package (`jrsonnet`) is not yet published.
+
=== Python
image:https://img.shields.io/pypi/v/rjsonnet[alt=crates.io, link=https://pypi.org/project/rjsonnet/]
@@ -100,6 +115,6 @@
=== Other
-WASM bingings are also available, Java bindings (Both JNI and WASM compiled to `.class`) are in progress
+Java bindings (Both JNI and WASM compiled to `.class`) are in progress
See link:./bindings/[bindings] for more information.
bindings/jrsonnet-web/Cargo.tomldiffbeforeafterboth--- a/bindings/jrsonnet-web/Cargo.toml
+++ b/bindings/jrsonnet-web/Cargo.toml
@@ -1,6 +1,8 @@
[package]
name = "jrsonnet-web"
-description = "WASM JS bindings for jrsonnet"
+description = "WebAssembly JavaScript bindings for jrsonnet"
+keywords = ["jsonnet", "wasm", "web"]
+categories = ["wasm"]
authors.workspace = true
edition.workspace = true
license.workspace = true
@@ -17,6 +19,7 @@
jrsonnet-stdlib.workspace = true
jrsonnet-types.workspace = true
js-sys.workspace = true
+rustc-hash.workspace = true
url.workspace = true
wasm-bindgen.workspace = true
wasm-bindgen-futures.workspace = true
bindings/jrsonnet-web/deno.jsondiffbeforeafterboth--- a/bindings/jrsonnet-web/deno.json
+++ b/bindings/jrsonnet-web/deno.json
@@ -1,7 +1,7 @@
{
"name": "@jrsonnet/jrsonnet",
"license": "MIT",
- "version": "0.0.1",
+ "version": "0.0.2",
"tasks": {
"wasmbuild": "deno run -A @deno/wasmbuild -p jrsonnet-web --skip-opt"
},
@@ -11,11 +11,15 @@
"@std/assert": "jsr:@std/assert@^1.0.19"
},
"exports": {
- ".": "./mod.ts"
+ ".": "./mod.ts",
+ "./fmt": "./fmt.ts"
},
"publish": {
"exclude": [
"!lib"
]
+ },
+ "fmt": {
+ "useTabs": true
}
}
bindings/jrsonnet-web/fmt.test.tsdiffbeforeafterboth--- a/bindings/jrsonnet-web/fmt.test.ts
+++ b/bindings/jrsonnet-web/fmt.test.ts
@@ -1,7 +1,7 @@
import { assertEquals } from "@std/assert";
-import { format, FormatOptions } from "./mod.ts";
+import { format, FormatOptions } from "./fmt.ts";
Deno.test("format", () => {
- const opts = new FormatOptions();
- assertEquals(format("{a:1+1}", opts), "{ a: 1 + 1 }\n");
+ const opts = new FormatOptions();
+ assertEquals(format("{a:1+1}", opts), "{ a: 1 + 1 }\n");
});
bindings/jrsonnet-web/fmt.tsdiffbeforeafterboth--- a/bindings/jrsonnet-web/fmt.ts
+++ b/bindings/jrsonnet-web/fmt.ts
@@ -0,0 +1 @@
+export { format, FormatOptions } from "./lib/jsonnet_web.js";
bindings/jrsonnet-web/mod.test.tsdiffbeforeafterboth--- a/bindings/jrsonnet-web/mod.test.ts
+++ b/bindings/jrsonnet-web/mod.test.ts
@@ -1,8 +1,139 @@
-import { assertEquals } from "@std/assert";
-import { WasmState } from "./mod.ts";
+import { assertEquals, assertRejects, assertThrows } from "@std/assert";
+import { type ImportResolver, JrsonnetError, State, ValKind } from "./mod.ts";
+
+Deno.test("evaluateSnippet returns numbers", () => {
+ const state = new State();
+ const v = state.evaluateSnippet("test.jsonnet", "1 + 2");
+ assertEquals(v.kind, ValKind.Num);
+ assertEquals(v.asNum(), 3);
+});
+
+Deno.test("evaluateSnippet returns booleans", () => {
+ const state = new State();
+ const v = state.evaluateSnippet("test.jsonnet", "true && !false");
+ assertEquals(v.kind, ValKind.Bool);
+ assertEquals(v.asBool(), true);
+});
+
+Deno.test("evaluateSnippet returns strings", () => {
+ const state = new State();
+ const v = state.evaluateSnippet("test.jsonnet", "'hello ' + 'world'");
+ assertEquals(v.kind, ValKind.Str);
+ assertEquals(v.asString(), "hello world");
+});
-Deno.test("basic", () => {
- const state = new WasmState();
+Deno.test("evaluateSnippet returns null", () => {
+ const state = new State();
+ const v = state.evaluateSnippet("test.jsonnet", "null");
+ assertEquals(v.kind, ValKind.Null);
+ assertEquals(v.asNum(), undefined);
+});
- assertEquals(state.evaluate_snippet("test.jsonnet", "1 + 2").as_num(), 3);
+Deno.test("Val.asArr exposes ArrValue", () => {
+ const state = new State();
+ const arr = state.evaluateSnippet("test.jsonnet", "[10, 20, 30]").asArr();
+ if (!arr) throw new Error("expected array");
+ assertEquals(arr.length, 3);
+ assertEquals(arr.at(1)?.asNum(), 20);
+ assertEquals(arr.at(99), undefined);
+});
+
+Deno.test("Val.asObj exposes ObjValue", () => {
+ const state = new State();
+ const obj = state.evaluateSnippet("test.jsonnet", "{a: 1, b: 'two'}").asObj();
+ if (!obj) throw new Error("expected object");
+ assertEquals(obj.keys().sort(), ["a", "b"]);
+ assertEquals(obj.get("a")?.asNum(), 1);
+ assertEquals(obj.get("b")?.asString(), "two");
+ assertEquals(obj.get("missing"), undefined);
+});
+
+Deno.test("evaluateSnippet manifests JSON", () => {
+ const state = new State();
+ const v = state.evaluateSnippet("test.jsonnet", "{a: 1, b: [2, 3]}");
+ assertEquals(v.manifestJson(0), '{"a":1,"b":[2,3]}');
+});
+
+Deno.test("evaluateSnippet propagates jsonnet errors", () => {
+ const state = new State();
+ assertThrows(() => state.evaluateSnippet("test.jsonnet", "error 'boom'"));
+});
+
+Deno.test("evaluateFile without resolver rejects", async () => {
+ const state = new State();
+ await assertRejects(() => state.evaluateFile("anything.jsonnet"));
+});
+
+Deno.test("resolver errors become JrsonnetError with cause", async () => {
+ const original = new Error("disk on fire");
+ const resolver: ImportResolver = {
+ resolveFrom(_from, path) {
+ return Promise.resolve(`memory:///${path}`);
+ },
+ loadFileContents(_resolved) {
+ throw original;
+ },
+ };
+ const state = new State(resolver);
+ const err = await assertRejects(
+ () => state.evaluateFile("anything.jsonnet"),
+ JrsonnetError,
+ "loadFileContents",
+ );
+ assertEquals(err.cause, original);
+ assertEquals(err.frames[0]?.desc, "loadFileContents");
+ // The wrapped error's own stack must not mention internal wasm frames.
+ assertEquals((err.stack ?? "").includes(".wasm"), false);
+});
+
+Deno.test("Val.applyTla calls function with named args", () => {
+ const state = new State();
+ const fn = state.evaluateSnippet(
+ "test.jsonnet",
+ "function(x, y) x + y",
+ );
+ const result = fn.applyTla({
+ x: state.evaluateSnippet("x.jsonnet", "10"),
+ y: state.evaluateSnippet("y.jsonnet", "32"),
+ });
+ assertEquals(result.asNum(), 42);
+});
+
+Deno.test("Val.applyTla borrows args without consuming them", () => {
+ const state = new State();
+ const fn = state.evaluateSnippet("test.jsonnet", "function(x) x * 2");
+ const x = state.evaluateSnippet("x.jsonnet", "21");
+ assertEquals(fn.applyTla({ x }).asNum(), 42);
+ assertEquals(x.asNum(), 21);
+ assertEquals(fn.applyTla({ x }).asNum(), 42);
+});
+
+Deno.test("Val.applyTla on non-function returns the value unchanged", () => {
+ const state = new State();
+ const v = state.evaluateSnippet("test.jsonnet", "123");
+ assertEquals(v.applyTla({}).asNum(), 123);
+});
+
+Deno.test("evaluateFileFrom resolves relative paths", async () => {
+ const files: Record<string, string> = {
+ "memory:///root/main.jsonnet": "import 'lib.jsonnet'",
+ "memory:///root/lib.jsonnet": "{ answer: 42 }",
+ };
+ const resolver: ImportResolver = {
+ resolveFrom(from, path) {
+ const base = from ?? "memory:///root/";
+ return Promise.resolve(new URL(path, base).toString());
+ },
+ loadFileContents(resolved) {
+ const code = files[resolved];
+ if (code === undefined) throw new Error(`missing ${resolved}`);
+ return Promise.resolve(new TextEncoder().encode(code));
+ },
+ };
+ const state = new State(resolver);
+ const v = await state.evaluateFileFrom(
+ "memory:///root/main.jsonnet",
+ "./lib.jsonnet",
+ );
+ assertEquals(v.asObj()?.get("answer")?.asNum(), 42);
});
bindings/jrsonnet-web/mod.tsdiffbeforeafterboth--- a/bindings/jrsonnet-web/mod.ts
+++ b/bindings/jrsonnet-web/mod.ts
@@ -1,60 +1,71 @@
import { assert } from "@std/assert";
import {
- format as formatRaw,
- type ImportResolver,
- WasmFormatOptions,
- WasmState,
- WasmVal,
+ ArrValue,
+ type ImportResolver,
+ ObjValue,
+ setErrorFactory,
+ State,
+ Val,
+ ValKind,
} from "./lib/jsonnet_web.js";
-export { type ImportResolver, WasmFormatOptions, WasmState, WasmVal };
+export interface JrsonnetFrame {
+ desc: string;
+ path?: string;
+ line?: number;
+ column?: number;
+}
-class FetchImportResolver implements ImportResolver {
- constructor(public base: string) {}
+export class JrsonnetError extends Error {
+ override name = "JrsonnetError" as const;
+ frames: JrsonnetFrame[];
- resolution = new Map<string, URL>();
- response = new Map<string, Response>();
+ constructor(message: string, frames: JrsonnetFrame[], cause?: unknown) {
+ super(message, cause !== undefined ? { cause } : undefined);
+ this.frames = frames;
+ }
+}
- async resolveFrom(from: string | undefined, path: string): Promise<string> {
- let resolved: URL;
- if (from) {
- resolved = new URL(path, from);
- } else {
- resolved = new URL(path, this.base);
- }
- const resolvingStr = resolved.toString();
- resolved = this.resolution.get(resolvingStr) ?? resolved;
+setErrorFactory(
+ (message: string, frames: JrsonnetFrame[], cause: unknown) =>
+ new JrsonnetError(message, frames, cause),
+);
+
+export { ArrValue, type ImportResolver, ObjValue, State, Val, ValKind };
- const resolvedStr = resolved.toString();
- if (!this.response.has(resolvedStr)) {
- console.log(resolved);
- const v = await fetch(resolved);
- this.response.set(resolvedStr, v);
- resolved = new URL(v.url);
- this.resolution.set(resolvingStr, resolved);
- }
- return resolved.toString();
- }
- loadFileContents(resolved: string): Promise<Uint8Array> {
- console.log(resolved);
- const v = this.response.get(resolved);
- assert(v, "should be resolved");
- return v.bytes();
- }
-}
+export class FetchImportResolver implements ImportResolver {
+ constructor(base: URL | string) {
+ this.#base = new URL(base);
+ }
-//
-// try {
-// console.log("eval file");
-// await state.evaluate_file("example.jsonnet");
-// console.log("eval file done");
-// } catch (e) {
-// console.log(e);
-// }
-//
-export function format(
- code: string,
- opts: WasmFormatOptions = new WasmFormatOptions(),
-): string {
- return formatRaw(code, opts);
+ #base: URL;
+ #resolution = new Map<string, string>();
+ #bytes = new Map<string, Uint8Array>();
+
+ async resolveFrom(from: string | undefined, path: string): Promise<string> {
+ const base = from !== undefined ? from : this.#base;
+ const requestStr = new URL(path, base).toString();
+
+ const cached = this.#resolution.get(requestStr);
+ if (cached !== undefined) return cached;
+
+ const resp = await fetch(requestStr);
+ if (!resp.ok) {
+ throw new Error(
+ `fetch ${requestStr}: HTTP ${resp.status} ${resp.statusText}`,
+ );
+ }
+ const canonical = resp.url;
+ if (!this.#bytes.has(canonical)) {
+ this.#bytes.set(canonical, await resp.bytes());
+ }
+ this.#resolution.set(requestStr, canonical);
+ return canonical;
+ }
+
+ loadFileContents(resolved: string): Promise<Uint8Array> {
+ const bytes = this.#bytes.get(resolved);
+ assert(bytes, `not loaded: ${resolved}`);
+ return Promise.resolve(bytes);
+ }
}
bindings/jrsonnet-web/scripts/build_npm.tsdiffbeforeafterboth--- a/bindings/jrsonnet-web/scripts/build_npm.ts
+++ b/bindings/jrsonnet-web/scripts/build_npm.ts
@@ -3,27 +3,27 @@
await emptyDir("./npm");
await build({
- entryPoints: ["./mod.ts"],
- outDir: "./npm",
- shims: {
- // see JS docs for overview and more options
- deno: true,
- },
- package: {
- // package.json properties
- name: "jrsonnet",
- version: Deno.args[0],
- description: "Jrsonnet.",
- license: "MIT",
- repository: {
- type: "git",
- url: "git+https://github.com/CertainLach/jrsonnet.git",
- },
- bugs: {
- url: "https://github.com/CertainLach/jrsonnet/issues",
- },
- },
- postBuild() {
- Deno.copyFileSync("../../LICENSE", "npm/LICENSE");
- },
+ entryPoints: ["./mod.ts"],
+ outDir: "./npm",
+ shims: {
+ // see JS docs for overview and more options
+ deno: true,
+ },
+ package: {
+ // package.json properties
+ name: "jrsonnet",
+ version: Deno.args[0],
+ description: "Jrsonnet.",
+ license: "MIT",
+ repository: {
+ type: "git",
+ url: "git+https://github.com/CertainLach/jrsonnet.git",
+ },
+ bugs: {
+ url: "https://github.com/CertainLach/jrsonnet/issues",
+ },
+ },
+ postBuild() {
+ Deno.copyFileSync("../../LICENSE", "npm/LICENSE");
+ },
});
bindings/jrsonnet-web/src/lib.rsdiffbeforeafterboth1use std::result::Result;23use jrsonnet_evaluator::{4 NumValue, Result as JrResult, SourcePath, SourceUrl, State, StateBuilder, Val,5 async_import::{ResolvedImportResolver, async_import},6 error,7 function::builtin::{NativeCallback, NativeCallbackHandler},8 manifest::{JsonFormat, ManifestFormat, StringFormat, ToStringFormat, YamlStreamFormat},9 trace::{JsFormat, PathResolver, TraceFormat},10 with_state,11};12use jrsonnet_formatter::FormatOptions;13use jrsonnet_gcmodule::Trace;14use jrsonnet_stdlib::{IniFormat, TomlFormat, XmlJsonmlFormat, YamlFormat};15use jrsonnet_types::ValType;16use wasm_bindgen::prelude::*;1718#[wasm_bindgen]19#[derive(Clone, Copy)]20pub enum ValKind {21 Null,22 Bool,23 Num,24 Str,25 Arr,26 Obj,27 Func,28}2930#[wasm_bindgen(inline_js = r"31export class JrsonnetError extends Error {32 constructor(message, frames) {33 super(message);34 this.name = 'JrsonnetError';35 this.frames = frames;36 }37}38export function makeJrsonnetError(message, frames) {39 return new JrsonnetError(message, frames);40}41")]42extern "C" {43 #[wasm_bindgen(js_name = makeJrsonnetError)]44 fn make_jrsonnet_error(message: &str, frames: js_sys::Array) -> JsValue;45}4647#[wasm_bindgen(typescript_custom_section)]48const TS_JRSONNET_ERROR: &'static str = r"49export interface JrsonnetFrame {50 desc: string;51 path?: string;52 line?: number;53 column?: number;54}55export class JrsonnetError extends Error {56 name: 'JrsonnetError';57 frames: JrsonnetFrame[];58}59";6061fn jrsonnet_js_error(e: &jrsonnet_evaluator::Error) -> JsValue {62 let msg = e.error().to_string();63 // let msg = format.format(e).unwrap_or_else(|_| e.to_string());64 let frames = js_sys::Array::new();65 for el in &e.trace().0 {66 let frame = js_sys::Object::new();67 let _ = js_sys::Reflect::set(68 &frame,69 &JsValue::from_str("desc"),70 &JsValue::from_str(&el.desc),71 );72 if let Some(loc) = &el.location {73 let path = loc.0.source_path().to_string();74 let _ = js_sys::Reflect::set(75 &frame,76 &JsValue::from_str("path"),77 &JsValue::from_str(&path),78 );79 let mapped = loc.0.map_source_locations(&[loc.1, loc.2]);80 let _ = js_sys::Reflect::set(81 &frame,82 &JsValue::from_str("line"),83 &JsValue::from(mapped[0].line),84 );85 let _ = js_sys::Reflect::set(86 &frame,87 &JsValue::from_str("column"),88 &JsValue::from(mapped[0].column),89 );90 }91 frames.push(&frame);92 }93 make_jrsonnet_error(&msg, frames)94}9596impl From<ValType> for ValKind {97 fn from(v: ValType) -> Self {98 match v {99 ValType::Null => Self::Null,100 ValType::Bool => Self::Bool,101 ValType::Num => Self::Num,102 ValType::Str => Self::Str,103 ValType::Arr => Self::Arr,104 ValType::Obj => Self::Obj,105 ValType::Func => Self::Func,106 }107 }108}109110#[wasm_bindgen]111pub struct WasmVal {112 val: Val,113 state: Option<State>,114}115116impl WasmVal {117 fn new(val: Val) -> Self {118 Self { val, state: None }119 }120 fn with_state(val: Val, state: State) -> Self {121 Self {122 val,123 state: Some(state),124 }125 }126 fn child(&self, val: Val) -> Self {127 Self {128 val,129 state: self.state.clone(),130 }131 }132 fn run<R>(&self, f: impl FnOnce(&Val) -> R) -> R {133 if let Some(state) = &self.state {134 let _guard = state.try_enter();135 f(&self.val)136 } else {137 f(&self.val)138 }139 }140 fn manifest_with(&self, format: impl ManifestFormat) -> Result<String, JsValue> {141 self.run(|v| v.manifest(format))142 .map_err(|e| jrsonnet_js_error(&e))143 }144}145146#[wasm_bindgen]147impl WasmVal {148 pub fn null() -> Self {149 Self::new(Val::Null)150 }151 pub fn bool(b: bool) -> Self {152 Self::new(Val::Bool(b))153 }154 pub fn num(n: f64) -> Result<Self, JsError> {155 let n = NumValue::new(n)156 .ok_or_else(|| JsError::new("only finite numbers are supported by jsonnet"))?;157 Ok(Self::new(Val::num(n)))158 }159 pub fn string(s: String) -> Self {160 Self::new(Val::string(s))161 }162 pub fn arr(items: Vec<WasmVal>) -> Self {163 Self::new(Val::arr(164 items.into_iter().map(|v| v.val).collect::<Vec<_>>(),165 ))166 }167 pub fn func(168 params: Vec<String>,169170 #[wasm_bindgen(unchecked_param_type = "(...args: WasmVal[]) => WasmVal")]171 callback: js_sys::Function,172 ) -> Self {173 #[allow(deprecated)]174 Self::new(Val::function(NativeCallback::new(175 params,176 JsHandler { func: callback },177 )))178 }179180 #[wasm_bindgen(getter)]181 pub fn kind(&self) -> ValKind {182 self.val.value_type().into()183 }184 pub fn as_bool(&self) -> Option<bool> {185 self.val.as_bool()186 }187 pub fn as_num(&self) -> Option<f64> {188 self.val.as_num()189 }190 pub fn as_string(&self) -> Option<String> {191 self.val.as_str().map(|s| s.to_string())192 }193 pub fn arr_len(&self) -> Option<u32> {194 self.val.as_arr().map(|a| a.len())195 }196 pub fn arr_at(&self, index: u32) -> Result<Option<WasmVal>, JsValue> {197 let Some(a) = self.val.as_arr() else {198 return Ok(None);199 };200 self.run(|_| a.get(index))201 .map(|opt| opt.map(|v| self.child(v)))202 .map_err(|e| jrsonnet_js_error(&e))203 }204 pub fn obj_keys(&self) -> Option<Vec<String>> {205 self.val206 .as_obj()207 .map(|o| o.fields().into_iter().map(|s| s.to_string()).collect())208 }209 pub fn obj_get(&self, key: String) -> Result<Option<WasmVal>, JsValue> {210 let Some(o) = self.val.as_obj() else {211 return Ok(None);212 };213 self.run(|_| o.get(key.into()))214 .map(|opt| opt.map(|v| self.child(v)))215 .map_err(|e| jrsonnet_js_error(&e))216 }217218 pub fn manifest_json(&self, indent: u32) -> Result<String, JsValue> {219 self.manifest_with(JsonFormat::cli(indent as usize))220 }221 pub fn manifest_to_string(&self) -> Result<String, JsValue> {222 self.manifest_with(ToStringFormat)223 }224 pub fn manifest_string(&self) -> Result<String, JsValue> {225 self.manifest_with(StringFormat)226 }227 pub fn manifest_yaml(&self, indent: u32, quote_keys: bool) -> Result<String, JsValue> {228 self.manifest_with(YamlFormat::std_to_yaml(indent != 0, quote_keys))229 }230 pub fn manifest_yaml_stream(231 &self,232 indent: u32,233 quote_keys: bool,234 c_document_end: bool,235 ) -> Result<String, JsValue> {236 self.manifest_with(YamlStreamFormat::std_yaml_stream(237 YamlFormat::std_to_yaml(indent != 0, quote_keys),238 c_document_end,239 ))240 }241 pub fn manifest_xml_jsonml(&self) -> Result<String, JsValue> {242 self.manifest_with(XmlJsonmlFormat::std_to_xml())243 }244 pub fn manifest_toml(&self, indent: u32) -> Result<String, JsValue> {245 self.manifest_with(TomlFormat::std_to_toml(" ".repeat(indent as usize)))246 }247 pub fn manifest_ini(&self) -> Result<String, JsValue> {248 self.manifest_with(IniFormat::std())249 }250}251252#[derive(Trace)]253struct JsHandler {254 #[trace(skip)]255 func: js_sys::Function,256}257258#[wasm_bindgen(inline_js = r"259export function js_invoke_val_callback(cb, args) {260 return cb.apply(null, args);261}262")]263extern "C" {264 #[wasm_bindgen(catch)]265 fn js_invoke_val_callback(266 cb: &js_sys::Function,267 args: &js_sys::Array,268 ) -> Result<WasmVal, JsValue>;269}270271impl NativeCallbackHandler for JsHandler {272 fn call(&self, args: &[Val]) -> JrResult<Val> {273 let js_args = js_sys::Array::new();274 let state = with_state(|s| s);275 for arg in args {276 js_args.push(&JsValue::from(WasmVal::with_state(277 arg.clone(),278 state.clone(),279 )));280 }281 let result = js_invoke_val_callback(&self.func, &js_args).map_err(|e| {282 let msg = e283 .as_string()284 .or_else(|| {285 e.dyn_ref::<js_sys::Error>()286 .map(|err| String::from(err.message()))287 })288 .unwrap_or_else(|| format!("{e:?}"));289 error!("js callback threw: {msg}")290 })?;291 Ok(result.val)292 }293}294295#[wasm_bindgen]296pub struct WasmState {297 state: State,298 resolver: JsAsyncResolver,299}300#[wasm_bindgen]301impl WasmState {302 #[wasm_bindgen(constructor)]303 pub fn new(resolver: ImportResolverJs) -> Self {304 console_error_panic_hook::set_once();305 let mut state = StateBuilder::default();306 state.import_resolver(ResolvedImportResolver::new());307 let std = jrsonnet_stdlib::ContextInitializer::new(PathResolver::Absolute);308 state.context_initializer(std);309 let state = state.build();310 Self {311 state,312 resolver: JsAsyncResolver { js: resolver },313 }314 }315316 #[wasm_bindgen]317 pub fn evaluate_snippet(&self, name: &str, snippet: &str) -> Result<WasmVal, JsValue> {318 let _guard = self.state.enter();319 self.state320 .evaluate_snippet(name, snippet)321 .map(|v| WasmVal::with_state(v, self.state.clone()))322 .map_err(|e| jrsonnet_js_error(&e))323 }324325 pub async fn evaluate_file(&self, path: String) -> Result<WasmVal, JsValue> {326 let path = async_import(self.state.clone(), self.resolver.clone(), &path.as_str()).await?;327 let _guard = self.state.enter();328 self.state329 .import_resolved(path)330 .map(|v| WasmVal::with_state(v, self.state.clone()))331 .map_err(|e| jrsonnet_js_error(&e))332 }333}334335#[wasm_bindgen]336extern "C" {337 #[wasm_bindgen(typescript_type = "ImportResolver")]338 #[derive(Clone)]339 pub type ImportResolverJs;340341 #[wasm_bindgen(catch, method, structural, js_name = resolveFrom)]342 fn resolve_from(343 this: &ImportResolverJs,344 from: Option<String>,345 path: &str,346 ) -> Result<js_sys::Promise, JsValue>;347348 #[wasm_bindgen(catch, method, structural, js_name = loadFileContents)]349 fn load_file_contents(350 this: &ImportResolverJs,351 resolved: &str,352 ) -> Result<js_sys::Promise, JsValue>;353}354355#[wasm_bindgen(typescript_custom_section)]356const TS_IMPORT_RESOLVER: &'static str = r"357export interface ImportResolver {358 resolveFrom(from: string | undefined, path: string): Promise<string>;359 loadFileContents(resolved: string): Promise<Uint8Array>;360}361";362363#[derive(Clone)]364struct JsAsyncResolver {365 js: ImportResolverJs,366}367368impl jrsonnet_evaluator::async_import::AsyncImportResolver for JsAsyncResolver {369 type Error = JsValue;370371 async fn resolve_from(372 &self,373 from: &SourcePath,374 path: &dyn jrsonnet_evaluator::AsPathLike,375 ) -> Result<SourcePath, JsValue> {376 let from_js = (!from.is_default()).then(|| from.to_string());377 let path_str = path.as_path().as_ref().to_string_lossy().into_owned();378 let promise = self.js.resolve_from(from_js, &path_str)?;379 let resolved_js = wasm_bindgen_futures::JsFuture::from(promise).await?;380 let resolved_str = resolved_js381 .as_string()382 .ok_or_else(|| JsValue::from_str("resolveFrom must return string"))?;383 let url = url::Url::parse(&resolved_str).map_err(|e| JsValue::from_str(&e.to_string()))?;384 Ok(SourcePath::new(SourceUrl::new(url)))385 }386387 async fn load_file_contents(&self, resolved: &SourcePath) -> Result<Vec<u8>, JsValue> {388 let resolved_str = resolved.to_string();389 let promise = self.js.load_file_contents(&resolved_str)?;390 let bytes_js = wasm_bindgen_futures::JsFuture::from(promise).await?;391 let arr = bytes_js392 .dyn_into::<js_sys::Uint8Array>()393 .map_err(|_| JsValue::from_str("loadFileContents must return Uint8Array"))?;394 Ok(arr.to_vec())395 }396}397398#[wasm_bindgen]399pub struct WasmFormatOptions {}400#[wasm_bindgen]401impl WasmFormatOptions {402 #[wasm_bindgen(constructor)]403 pub fn new() -> Self {404 Self {}405 }406407 fn build(&self) -> FormatOptions {408 FormatOptions { indent: 0 }409 }410}411412#[wasm_bindgen]413pub fn format(src: &str, opts: &WasmFormatOptions) -> Result<String, String> {414 match jrsonnet_formatter::format(src, &opts.build()) {415 Ok(v) => Ok(v),416 Err(e) => {417 let e = e.build();418 Err(hi_doc::source_to_ansi(&e))419 }420 }421}