git.delta.rocks / jrsonnet / refs/commits / 9aabcd49c223

difftreelog

refactor(web) idiomatic js api

zwxrzuuxYaroslav Bolyukin2026-05-05parent: #6ac56ea.patch.diff
in: master

10 files changed

modifiedCargo.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",
modifiedREADME.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.
modifiedbindings/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
modifiedbindings/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
 	}
 }
modifiedbindings/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");
 });
modifiedbindings/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";
modifiedbindings/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);
 });
modifiedbindings/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);
+	}
 }
modifiedbindings/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");
+	},
 });
modifiedbindings/jrsonnet-web/src/lib.rsdiffbeforeafterboth
before · bindings/jrsonnet-web/src/lib.rs
1use 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}
after · bindings/jrsonnet-web/src/lib.rs
1#![allow(clippy::future_not_send, reason = "we work with js promises anyway")]23use std::{cell::RefCell, result::Result};45use jrsonnet_evaluator::{6	IStr, NumValue, ObjValue, Result as JrResult, SourcePath, SourceUrl, State, StateBuilder, Val,7	async_import::{ResolvedImportResolver, async_import},8	error,9	function::builtin::{NativeCallback, NativeCallbackHandler},10	manifest::{JsonFormat, ManifestFormat, StringFormat, ToStringFormat, YamlStreamFormat},11	tla::{TlaArg, apply_tla},12	trace::PathResolver,13	val::ArrValue,14	with_state,15};16use jrsonnet_formatter::FormatOptions;17use jrsonnet_gcmodule::Trace;18use jrsonnet_stdlib::{IniFormat, TomlFormat, XmlJsonmlFormat, YamlFormat};19use jrsonnet_types::ValType;20use js_sys::Reflect::get;21use rustc_hash::FxHashMap;22use wasm_bindgen::{convert::RefFromWasmAbi, prelude::*};2324#[wasm_bindgen]25#[derive(Clone, Copy)]26pub enum ValKind {27	Null,28	Bool,29	Num,30	Str,31	Arr,32	Obj,33	Func,34}3536thread_local! {37	static ERR_FACTORY: RefCell<Option<js_sys::Function>> = const { RefCell::new(None) };38}39#[wasm_bindgen(js_name = setErrorFactory)]40pub fn set_error_factory(f: js_sys::Function) {41	ERR_FACTORY.with(|c| *c.borrow_mut() = Some(f));42}43fn make_jrsonnet_error(message: &str, frames: js_sys::Array, cause: &JsValue) -> JsValue {44	ERR_FACTORY.with(|c| {45		c.borrow().as_ref().map_or_else(46			|| js_sys::Error::new(message).into(),47			|f| {48				let args = js_sys::Array::new();49				args.push(&JsValue::from_str(message));50				args.push(&frames);51				args.push(cause);52				f.apply(&JsValue::NULL, &args)53					.unwrap_or_else(|e| js_sys::Error::new(&format!("{e:?}")).into())54			},55		)56	})57}5859fn js_error_message(e: &JsValue) -> String {60	e.dyn_ref::<js_sys::Error>().map_or_else(61		|| e.as_string().unwrap_or_else(|| format!("{e:?}")),62		|err| String::from(err.message()),63	)64}6566fn unwrap_val_ref(value: &JsValue) -> Result<<WasmVal as RefFromWasmAbi>::Anchor, JsValue> {67	let ptr = get(value, &JsValue::from_str("__wbg_ptr"))68		.ok()69		.and_then(|v| v.as_f64())70		.ok_or_else(|| JsValue::from_str("expected a Val instance"))? as u32;71	if ptr == 0 {72		return Err(JsValue::from_str("Val has been freed"));73	}74	Ok(unsafe { <WasmVal as RefFromWasmAbi>::ref_from_abi(ptr) })75}7677fn js_resolver_error(prefix: &str, e: JsValue) -> JsValue {78	let msg = format!("{prefix}: {}", js_error_message(&e));79	let frames = js_sys::Array::new();80	let frame = js_sys::Object::new();81	let _ = js_sys::Reflect::set(82		&frame,83		&JsValue::from_str("desc"),84		&JsValue::from_str(prefix),85	);86	frames.push(&frame);87	make_jrsonnet_error(&msg, frames, &e)88}8990fn jrsonnet_js_error(e: &jrsonnet_evaluator::Error) -> JsValue {91	let msg = e.error().to_string();92	// let msg = format.format(e).unwrap_or_else(|_| e.to_string());93	let frames = js_sys::Array::new();94	for el in &e.trace().0 {95		let frame = js_sys::Object::new();96		let _ = js_sys::Reflect::set(97			&frame,98			&JsValue::from_str("desc"),99			&JsValue::from_str(&el.desc),100		);101		if let Some(loc) = &el.location {102			let path = loc.0.source_path().to_string();103			let _ = js_sys::Reflect::set(104				&frame,105				&JsValue::from_str("path"),106				&JsValue::from_str(&path),107			);108			let mapped = loc.0.map_source_locations(&[loc.1, loc.2]);109			let _ = js_sys::Reflect::set(110				&frame,111				&JsValue::from_str("line"),112				&JsValue::from(mapped[0].line),113			);114			let _ = js_sys::Reflect::set(115				&frame,116				&JsValue::from_str("column"),117				&JsValue::from(mapped[0].column),118			);119		}120		frames.push(&frame);121	}122	make_jrsonnet_error(&msg, frames, &JsValue::UNDEFINED)123}124125impl From<ValType> for ValKind {126	fn from(v: ValType) -> Self {127		match v {128			ValType::Null => Self::Null,129			ValType::Bool => Self::Bool,130			ValType::Num => Self::Num,131			ValType::Str => Self::Str,132			ValType::Arr => Self::Arr,133			ValType::Obj => Self::Obj,134			ValType::Func => Self::Func,135		}136	}137}138139#[wasm_bindgen(js_name = Val)]140pub struct WasmVal {141	val: Val,142	state: Option<State>,143}144145impl WasmVal {146	fn new(val: Val) -> Self {147		Self { val, state: None }148	}149	fn with_state(val: Val, state: State) -> Self {150		Self {151			val,152			state: Some(state),153		}154	}155	fn run<R>(&self, f: impl FnOnce(&Val) -> R) -> R {156		if let Some(state) = &self.state {157			let _guard = state.try_enter();158			f(&self.val)159		} else {160			f(&self.val)161		}162	}163	fn manifest_with(&self, format: impl ManifestFormat) -> Result<String, JsValue> {164		self.run(|v| v.manifest(format))165			.map_err(|e| jrsonnet_js_error(&e))166	}167}168169#[wasm_bindgen(js_class = Val)]170impl WasmVal {171	pub fn null() -> Self {172		Self::new(Val::Null)173	}174	pub fn bool(b: bool) -> Self {175		Self::new(Val::Bool(b))176	}177	pub fn num(n: f64) -> Result<Self, JsError> {178		let n = NumValue::new(n)179			.ok_or_else(|| JsError::new("only finite numbers are supported by jsonnet"))?;180		Ok(Self::new(Val::num(n)))181	}182	pub fn string(s: String) -> Self {183		Self::new(Val::string(s))184	}185	pub fn arr(items: Vec<WasmVal>) -> Self {186		Self::new(Val::arr(187			items.into_iter().map(|v| v.val).collect::<Vec<_>>(),188		))189	}190	pub fn func(191		params: Vec<String>,192193		#[wasm_bindgen(unchecked_param_type = "(...args: Val[]) => Val")]194		callback: js_sys::Function,195	) -> Self {196		#[allow(deprecated)]197		Self::new(Val::function(NativeCallback::new(198			params,199			JsHandler { func: callback },200		)))201	}202203	#[wasm_bindgen(getter)]204	pub fn kind(&self) -> ValKind {205		self.val.value_type().into()206	}207	#[wasm_bindgen(js_name = asBool)]208	pub fn as_bool(&self) -> Option<bool> {209		self.val.as_bool()210	}211	#[wasm_bindgen(js_name = asNum)]212	pub fn as_num(&self) -> Option<f64> {213		self.val.as_num()214	}215	#[wasm_bindgen(js_name = asString)]216	pub fn as_string(&self) -> Option<String> {217		self.val.as_str().map(|s| s.to_string())218	}219	#[wasm_bindgen(js_name = asArr)]220	pub fn as_arr(&self) -> Option<WasmArrValue> {221		self.val.as_arr().map(|arr| WasmArrValue {222			arr,223			state: self.state.clone(),224		})225	}226	#[wasm_bindgen(js_name = asObj)]227	pub fn as_obj(&self) -> Option<WasmObjValue> {228		self.val.as_obj().map(|obj| WasmObjValue {229			obj,230			state: self.state.clone(),231		})232	}233234	#[wasm_bindgen(js_name = applyTla)]235	pub fn apply_tla(236		&self,237		#[wasm_bindgen(unchecked_param_type = "Record<string, Val>")] args: &js_sys::Object,238	) -> Result<WasmVal, JsValue> {239		let mut map: FxHashMap<IStr, TlaArg> = FxHashMap::default();240		for entry in js_sys::Object::entries(args).iter() {241			let pair: js_sys::Array = entry242				.dyn_into()243				.map_err(|_| JsValue::from_str("expected [key, value] entry"))?;244			let key = pair245				.get(0)246				.as_string()247				.ok_or_else(|| JsValue::from_str("TLA arg key must be a string"))?;248			let value = unwrap_val_ref(&pair.get(1))?;249			map.insert(key.into(), TlaArg::Val(value.val.clone()));250		}251		let val = self.val.clone();252		self.run(|_| apply_tla(&map, val))253			.map(|v| WasmVal {254				val: v,255				state: self.state.clone(),256			})257			.map_err(|e| jrsonnet_js_error(&e))258	}259260	#[wasm_bindgen(js_name = manifestJson)]261	pub fn manifest_json(&self, indent: u32) -> Result<String, JsValue> {262		self.manifest_with(JsonFormat::cli(indent as usize))263	}264	#[wasm_bindgen(js_name = manifestToString)]265	pub fn manifest_to_string(&self) -> Result<String, JsValue> {266		self.manifest_with(ToStringFormat)267	}268	#[wasm_bindgen(js_name = manifestString)]269	pub fn manifest_string(&self) -> Result<String, JsValue> {270		self.manifest_with(StringFormat)271	}272	#[wasm_bindgen(js_name = manifestYaml)]273	pub fn manifest_yaml(&self, indent: u32, quote_keys: bool) -> Result<String, JsValue> {274		self.manifest_with(YamlFormat::std_to_yaml(indent != 0, quote_keys))275	}276	#[wasm_bindgen(js_name = manifestYamlStream)]277	pub fn manifest_yaml_stream(278		&self,279		indent: u32,280		quote_keys: bool,281		c_document_end: bool,282	) -> Result<String, JsValue> {283		self.manifest_with(YamlStreamFormat::std_yaml_stream(284			YamlFormat::std_to_yaml(indent != 0, quote_keys),285			c_document_end,286		))287	}288	#[wasm_bindgen(js_name = manifestXmlJsonml)]289	pub fn manifest_xml_jsonml(&self) -> Result<String, JsValue> {290		self.manifest_with(XmlJsonmlFormat::std_to_xml())291	}292	#[wasm_bindgen(js_name = manifestToml)]293	pub fn manifest_toml(&self, indent: u32) -> Result<String, JsValue> {294		self.manifest_with(TomlFormat::std_to_toml(" ".repeat(indent as usize)))295	}296	#[wasm_bindgen(js_name = manifestIni)]297	pub fn manifest_ini(&self) -> Result<String, JsValue> {298		self.manifest_with(IniFormat::std())299	}300}301302#[wasm_bindgen(js_name = ArrValue)]303pub struct WasmArrValue {304	arr: ArrValue,305	state: Option<State>,306}307308#[wasm_bindgen(js_class = ArrValue)]309impl WasmArrValue {310	#[wasm_bindgen(getter)]311	pub fn length(&self) -> u32 {312		self.arr.len()313	}314	pub fn at(&self, index: u32) -> Result<Option<WasmVal>, JsValue> {315		let result = self.state.as_ref().map_or_else(316			|| self.arr.get(index),317			|state| {318				let _guard = state.try_enter();319				self.arr.get(index)320			},321		);322		result323			.map(|opt: Option<Val>| {324				opt.map(|v| WasmVal {325					val: v,326					state: self.state.clone(),327				})328			})329			.map_err(|e| jrsonnet_js_error(&e))330	}331}332333#[wasm_bindgen(js_name = ObjValue)]334pub struct WasmObjValue {335	obj: ObjValue,336	state: Option<State>,337}338339#[wasm_bindgen(js_class = ObjValue)]340impl WasmObjValue {341	pub fn keys(&self) -> Vec<String> {342		self.obj343			.fields()344			.into_iter()345			.map(|s| s.to_string())346			.collect()347	}348	pub fn get(&self, key: String) -> Result<Option<WasmVal>, JsValue> {349		let result = if let Some(state) = &self.state {350			let _guard = state.try_enter();351			self.obj.get(key.into())352		} else {353			self.obj.get(key.into())354		};355		result356			.map(|opt: Option<Val>| {357				opt.map(|v| WasmVal {358					val: v,359					state: self.state.clone(),360				})361			})362			.map_err(|e| jrsonnet_js_error(&e))363	}364}365366#[derive(Trace)]367struct JsHandler {368	#[trace(skip)]369	func: js_sys::Function,370}371372#[wasm_bindgen(inline_js = r"373export function js_invoke_val_callback(cb, args) {374	return cb.apply(null, args);375}376")]377extern "C" {378	#[wasm_bindgen(catch)]379	fn js_invoke_val_callback(380		cb: &js_sys::Function,381		args: &js_sys::Array,382	) -> Result<WasmVal, JsValue>;383}384385impl NativeCallbackHandler for JsHandler {386	fn call(&self, args: &[Val]) -> JrResult<Val> {387		let js_args = js_sys::Array::new();388		let state = with_state(|s| s);389		for arg in args {390			js_args.push(&JsValue::from(WasmVal::with_state(391				arg.clone(),392				state.clone(),393			)));394		}395		let result = js_invoke_val_callback(&self.func, &js_args).map_err(|e| {396			let msg = e397				.as_string()398				.or_else(|| {399					e.dyn_ref::<js_sys::Error>()400						.map(|err| String::from(err.message()))401				})402				.unwrap_or_else(|| format!("{e:?}"));403			error!("js callback threw: {msg}")404		})?;405		Ok(result.val)406	}407}408409#[wasm_bindgen(js_name = State)]410pub struct WasmState {411	state: State,412	resolver: Option<JsAsyncResolver>,413}414#[wasm_bindgen(js_class = State)]415impl WasmState {416	#[wasm_bindgen(constructor)]417	pub fn new(resolver: Option<ImportResolverJs>) -> Self {418		console_error_panic_hook::set_once();419		let mut state = StateBuilder::default();420		state.import_resolver(ResolvedImportResolver::new());421		let std = jrsonnet_stdlib::ContextInitializer::new(PathResolver::Absolute);422		state.context_initializer(std);423		let state = state.build();424		Self {425			state,426			resolver: resolver.map(|js| JsAsyncResolver { js }),427		}428	}429430	#[wasm_bindgen(js_name = evaluateSnippet)]431	pub fn evaluate_snippet(&self, name: &str, snippet: &str) -> Result<WasmVal, JsValue> {432		let _guard = self.state.enter();433		self.state434			.evaluate_snippet(name, snippet)435			.map(|v| WasmVal::with_state(v, self.state.clone()))436			.map_err(|e| jrsonnet_js_error(&e))437	}438439	#[wasm_bindgen(js_name = evaluateFile)]440	pub async fn evaluate_file(&self, path: String) -> Result<WasmVal, JsValue> {441		self.evaluate_file_from_impl(None, path).await442	}443444	#[wasm_bindgen(js_name = evaluateFileFrom)]445	pub async fn evaluate_file_from(&self, from: String, path: String) -> Result<WasmVal, JsValue> {446		self.evaluate_file_from_impl(Some(from), path).await447	}448}449450impl WasmState {451	async fn evaluate_file_from_impl(452		&self,453		from: Option<String>,454		path: String,455	) -> Result<WasmVal, JsValue> {456		let resolver = self457			.resolver458			.clone()459			.ok_or_else(|| JsValue::from_str("file evaluation requires an ImportResolver"))?;460		let from = match from {461			Some(s) => {462				let url = url::Url::parse(&s).map_err(|e| JsValue::from_str(&e.to_string()))?;463				SourcePath::new(SourceUrl::new(url))464			}465			None => SourcePath::default(),466		};467		let path = async_import(self.state.clone(), resolver, &from, &path.as_str()).await?;468		let _guard = self.state.enter();469		self.state470			.import_resolved(path)471			.map(|v| WasmVal::with_state(v, self.state.clone()))472			.map_err(|e| jrsonnet_js_error(&e))473	}474}475476#[wasm_bindgen]477extern "C" {478	#[wasm_bindgen(typescript_type = "ImportResolver")]479	#[derive(Clone)]480	pub type ImportResolverJs;481482	#[wasm_bindgen(catch, method, structural, js_name = resolveFrom)]483	fn resolve_from(484		this: &ImportResolverJs,485		from: Option<String>,486		path: &str,487	) -> Result<js_sys::Promise, JsValue>;488489	#[wasm_bindgen(catch, method, structural, js_name = loadFileContents)]490	fn load_file_contents(491		this: &ImportResolverJs,492		resolved: &str,493	) -> Result<js_sys::Promise, JsValue>;494}495496#[wasm_bindgen(typescript_custom_section)]497const TS_IMPORT_RESOLVER: &'static str = r"498export interface ImportResolver {499	resolveFrom(from: string | undefined, path: string): Promise<string>;500	loadFileContents(resolved: string): Promise<Uint8Array>;501}502";503504#[derive(Clone)]505struct JsAsyncResolver {506	js: ImportResolverJs,507}508509impl jrsonnet_evaluator::async_import::AsyncImportResolver for JsAsyncResolver {510	type Error = JsValue;511512	async fn resolve_from(513		&self,514		from: &SourcePath,515		path: &dyn jrsonnet_evaluator::AsPathLike,516	) -> Result<SourcePath, JsValue> {517		let from_js = (!from.is_default()).then(|| from.to_string());518		let path_str = path.as_path().as_ref().to_string_lossy().into_owned();519		let promise = self520			.js521			.resolve_from(from_js, &path_str)522			.map_err(|e| js_resolver_error("resolveFrom", e))?;523		let resolved_js = wasm_bindgen_futures::JsFuture::from(promise)524			.await525			.map_err(|e| js_resolver_error("resolveFrom", e))?;526		let resolved_str = resolved_js527			.as_string()528			.ok_or_else(|| JsValue::from_str("resolveFrom must return string"))?;529		let url = url::Url::parse(&resolved_str).map_err(|e| JsValue::from_str(&e.to_string()))?;530		Ok(SourcePath::new(SourceUrl::new(url)))531	}532533	async fn load_file_contents(&self, resolved: &SourcePath) -> Result<Vec<u8>, JsValue> {534		let resolved_str = resolved.to_string();535		let promise = self536			.js537			.load_file_contents(&resolved_str)538			.map_err(|e| js_resolver_error("loadFileContents", e))?;539		let bytes_js = wasm_bindgen_futures::JsFuture::from(promise)540			.await541			.map_err(|e| js_resolver_error("loadFileContents", e))?;542		let arr = bytes_js543			.dyn_into::<js_sys::Uint8Array>()544			.map_err(|_| JsValue::from_str("loadFileContents must return Uint8Array"))?;545		Ok(arr.to_vec())546	}547}548549#[wasm_bindgen(js_name = FormatOptions)]550pub struct WasmFormatOptions {551	indent: u8,552}553#[wasm_bindgen(js_class = FormatOptions)]554impl WasmFormatOptions {555	#[wasm_bindgen(constructor)]556	pub fn new() -> Self {557		Self { indent: 0 }558	}559560	fn build(&self) -> FormatOptions {561		FormatOptions {562			indent: self.indent,563		}564	}565}566567impl Default for WasmFormatOptions {568	fn default() -> Self {569		Self::new()570	}571}572573#[wasm_bindgen]574pub fn format(src: &str, opts: &WasmFormatOptions) -> Result<String, String> {575	match jrsonnet_formatter::format(src, &opts.build()) {576		Ok(v) => Ok(v),577		Err(e) => {578			let e = e.build();579			Err(hi_doc::source_to_ansi(&e))580		}581	}582}