@barefootjs/rust 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +194 -0
  2. package/dist/adapter/analysis/component-tree.d.ts +26 -0
  3. package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
  4. package/dist/adapter/boolean-result.d.ts +85 -0
  5. package/dist/adapter/boolean-result.d.ts.map +1 -0
  6. package/dist/adapter/emit-context.d.ts +107 -0
  7. package/dist/adapter/emit-context.d.ts.map +1 -0
  8. package/dist/adapter/expr/array-method.d.ts +75 -0
  9. package/dist/adapter/expr/array-method.d.ts.map +1 -0
  10. package/dist/adapter/expr/emitters.d.ts +143 -0
  11. package/dist/adapter/expr/emitters.d.ts.map +1 -0
  12. package/dist/adapter/index.d.ts +6 -0
  13. package/dist/adapter/index.d.ts.map +1 -0
  14. package/dist/adapter/index.js +189091 -0
  15. package/dist/adapter/lib/constants.d.ts +25 -0
  16. package/dist/adapter/lib/constants.d.ts.map +1 -0
  17. package/dist/adapter/lib/ir-scope.d.ts +50 -0
  18. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  19. package/dist/adapter/lib/minijinja-naming.d.ts +64 -0
  20. package/dist/adapter/lib/minijinja-naming.d.ts.map +1 -0
  21. package/dist/adapter/lib/types.d.ts +32 -0
  22. package/dist/adapter/lib/types.d.ts.map +1 -0
  23. package/dist/adapter/memo/seed.d.ts +84 -0
  24. package/dist/adapter/memo/seed.d.ts.map +1 -0
  25. package/dist/adapter/minijinja-adapter.d.ts +421 -0
  26. package/dist/adapter/minijinja-adapter.d.ts.map +1 -0
  27. package/dist/adapter/props/prop-classes.d.ts +33 -0
  28. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  29. package/dist/adapter/spread/spread-codegen.d.ts +63 -0
  30. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  31. package/dist/adapter/value/parsed-literal.d.ts +28 -0
  32. package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
  33. package/dist/build.d.ts +29 -0
  34. package/dist/build.d.ts.map +1 -0
  35. package/dist/build.js +189111 -0
  36. package/dist/conformance-pins.d.ts +13 -0
  37. package/dist/conformance-pins.d.ts.map +1 -0
  38. package/dist/index.d.ts +12 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +189112 -0
  41. package/package.json +67 -0
  42. package/runtime/Cargo.lock +124 -0
  43. package/runtime/Cargo.toml +21 -0
  44. package/runtime/src/backend_minijinja.rs +176 -0
  45. package/runtime/src/bin/bf-render.rs +147 -0
  46. package/runtime/src/evaluator.rs +770 -0
  47. package/runtime/src/lib.rs +19 -0
  48. package/runtime/src/manifest.rs +258 -0
  49. package/runtime/src/num.rs +558 -0
  50. package/runtime/src/runtime.rs +1548 -0
  51. package/runtime/src/search_params.rs +173 -0
  52. package/runtime/tests/eval_vectors.rs +94 -0
  53. package/runtime/tests/evaluator.rs +407 -0
  54. package/runtime/tests/helper_vectors.rs +348 -0
  55. package/runtime/tests/manifest.rs +169 -0
  56. package/runtime/tests/omit.rs +79 -0
  57. package/runtime/tests/props_attr.rs +75 -0
  58. package/runtime/tests/query.rs +50 -0
  59. package/runtime/tests/render_child.rs +210 -0
  60. package/runtime/tests/search_params.rs +68 -0
  61. package/runtime/tests/spread_attrs.rs +94 -0
  62. package/runtime/tests/template_primitives.rs +376 -0
  63. package/runtime/tests/vector-divergences.json +33 -0
  64. package/src/__tests__/minijinja-adapter-unit.test.ts +392 -0
  65. package/src/__tests__/minijinja-adapter.test.ts +58 -0
  66. package/src/__tests__/minijinja-counter.test.ts +61 -0
  67. package/src/__tests__/minijinja-query-href.test.ts +101 -0
  68. package/src/__tests__/minijinja-spread-attrs.test.ts +227 -0
  69. package/src/adapter/analysis/component-tree.ts +119 -0
  70. package/src/adapter/boolean-result.ts +177 -0
  71. package/src/adapter/emit-context.ts +119 -0
  72. package/src/adapter/expr/array-method.ts +346 -0
  73. package/src/adapter/expr/emitters.ts +608 -0
  74. package/src/adapter/index.ts +6 -0
  75. package/src/adapter/lib/constants.ts +37 -0
  76. package/src/adapter/lib/ir-scope.ts +95 -0
  77. package/src/adapter/lib/minijinja-naming.ts +85 -0
  78. package/src/adapter/lib/types.ts +35 -0
  79. package/src/adapter/memo/seed.ts +135 -0
  80. package/src/adapter/minijinja-adapter.ts +1796 -0
  81. package/src/adapter/props/prop-classes.ts +65 -0
  82. package/src/adapter/spread/spread-codegen.ts +168 -0
  83. package/src/adapter/value/parsed-literal.ts +76 -0
  84. package/src/build.ts +38 -0
  85. package/src/conformance-pins.ts +101 -0
  86. package/src/index.ts +12 -0
  87. package/src/test-render.ts +680 -0
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@barefootjs/rust",
3
+ "version": "0.1.0",
4
+ "description": "minijinja (Rust) adapter for BarefootJS — compiles IR to .j2 templates and ships a Rust rendering runtime (packages/adapter-rust/runtime/); runs under any Rust web framework (axum, etc.)",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ },
13
+ "./adapter": {
14
+ "types": "./dist/adapter/index.d.ts",
15
+ "import": "./dist/adapter/index.js"
16
+ },
17
+ "./test-render": {
18
+ "bun": "./src/test-render.ts"
19
+ },
20
+ "./build": {
21
+ "types": "./dist/build.d.ts",
22
+ "import": "./dist/build.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "src",
28
+ "runtime",
29
+ "README.md"
30
+ ],
31
+ "scripts": {
32
+ "build": "bun run build:js && bun run build:types",
33
+ "build:js": "bun build ./src/index.ts ./src/adapter/index.ts ./src/build.ts --root ./src --outdir ./dist --format esm --external @barefootjs/jsx --external @barefootjs/shared",
34
+ "build:types": "tsgo --emitDeclarationOnly --outDir ./dist",
35
+ "test": "bun test",
36
+ "clean": "rm -rf dist",
37
+ "prepack": "node ../../scripts/swap-publish-config.mjs pack",
38
+ "postpack": "node ../../scripts/swap-publish-config.mjs unpack"
39
+ },
40
+ "keywords": [
41
+ "jinja",
42
+ "jinja2",
43
+ "minijinja",
44
+ "rust",
45
+ "axum",
46
+ "barefoot",
47
+ "ssr"
48
+ ],
49
+ "author": "kobaken <kentafly88@gmail.com>",
50
+ "license": "MIT",
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "https://github.com/piconic-ai/barefootjs",
54
+ "directory": "packages/adapter-rust"
55
+ },
56
+ "dependencies": {
57
+ "@barefootjs/shared": "0.17.1"
58
+ },
59
+ "peerDependencies": {
60
+ "@barefootjs/jsx": ">=0.2.0"
61
+ },
62
+ "devDependencies": {
63
+ "@barefootjs/adapter-tests": "0.1.0",
64
+ "@barefootjs/jsx": "0.17.1",
65
+ "typescript": "^5.0.0"
66
+ }
67
+ }
@@ -0,0 +1,124 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "barefootjs"
7
+ version = "0.1.0"
8
+ dependencies = [
9
+ "minijinja",
10
+ "serde",
11
+ "serde_json",
12
+ ]
13
+
14
+ [[package]]
15
+ name = "itoa"
16
+ version = "1.0.18"
17
+ source = "registry+https://github.com/rust-lang/crates.io-index"
18
+ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
19
+
20
+ [[package]]
21
+ name = "memchr"
22
+ version = "2.8.2"
23
+ source = "registry+https://github.com/rust-lang/crates.io-index"
24
+ checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
25
+
26
+ [[package]]
27
+ name = "memo-map"
28
+ version = "0.3.3"
29
+ source = "registry+https://github.com/rust-lang/crates.io-index"
30
+ checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b"
31
+
32
+ [[package]]
33
+ name = "minijinja"
34
+ version = "2.21.0"
35
+ source = "registry+https://github.com/rust-lang/crates.io-index"
36
+ checksum = "cb3d648e68cea56d9858d535ee28f9538404e2dd8cb08ed0bd05dca379477f39"
37
+ dependencies = [
38
+ "memo-map",
39
+ "serde",
40
+ ]
41
+
42
+ [[package]]
43
+ name = "proc-macro2"
44
+ version = "1.0.106"
45
+ source = "registry+https://github.com/rust-lang/crates.io-index"
46
+ checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
47
+ dependencies = [
48
+ "unicode-ident",
49
+ ]
50
+
51
+ [[package]]
52
+ name = "quote"
53
+ version = "1.0.46"
54
+ source = "registry+https://github.com/rust-lang/crates.io-index"
55
+ checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
56
+ dependencies = [
57
+ "proc-macro2",
58
+ ]
59
+
60
+ [[package]]
61
+ name = "serde"
62
+ version = "1.0.228"
63
+ source = "registry+https://github.com/rust-lang/crates.io-index"
64
+ checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
65
+ dependencies = [
66
+ "serde_core",
67
+ "serde_derive",
68
+ ]
69
+
70
+ [[package]]
71
+ name = "serde_core"
72
+ version = "1.0.228"
73
+ source = "registry+https://github.com/rust-lang/crates.io-index"
74
+ checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
75
+ dependencies = [
76
+ "serde_derive",
77
+ ]
78
+
79
+ [[package]]
80
+ name = "serde_derive"
81
+ version = "1.0.228"
82
+ source = "registry+https://github.com/rust-lang/crates.io-index"
83
+ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
84
+ dependencies = [
85
+ "proc-macro2",
86
+ "quote",
87
+ "syn",
88
+ ]
89
+
90
+ [[package]]
91
+ name = "serde_json"
92
+ version = "1.0.150"
93
+ source = "registry+https://github.com/rust-lang/crates.io-index"
94
+ checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
95
+ dependencies = [
96
+ "itoa",
97
+ "memchr",
98
+ "serde",
99
+ "serde_core",
100
+ "zmij",
101
+ ]
102
+
103
+ [[package]]
104
+ name = "syn"
105
+ version = "2.0.118"
106
+ source = "registry+https://github.com/rust-lang/crates.io-index"
107
+ checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
108
+ dependencies = [
109
+ "proc-macro2",
110
+ "quote",
111
+ "unicode-ident",
112
+ ]
113
+
114
+ [[package]]
115
+ name = "unicode-ident"
116
+ version = "1.0.24"
117
+ source = "registry+https://github.com/rust-lang/crates.io-index"
118
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
119
+
120
+ [[package]]
121
+ name = "zmij"
122
+ version = "1.0.21"
123
+ source = "registry+https://github.com/rust-lang/crates.io-index"
124
+ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
@@ -0,0 +1,21 @@
1
+ [package]
2
+ name = "barefootjs"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Rust runtime for BarefootJS marked templates (minijinja adapter)."
6
+ license = "MIT"
7
+
8
+ [lib]
9
+ name = "barefootjs"
10
+ path = "src/lib.rs"
11
+
12
+ [[bin]]
13
+ name = "bf-render"
14
+ path = "src/bin/bf-render.rs"
15
+
16
+ [dependencies]
17
+ minijinja = { version = "2", features = ["loader"] }
18
+ serde = { version = "1", features = ["derive"] }
19
+ serde_json = { version = "1", features = ["float_roundtrip"] }
20
+
21
+ [dev-dependencies]
@@ -0,0 +1,176 @@
1
+ //! minijinja `Environment` construction, the custom formatter, canonical
2
+ //! JSON encoding, and named-template rendering.
3
+ //!
4
+ //! Port of `packages/adapter-jinja/python/barefootjs/backend_jinja.py`'s
5
+ //! four engine-specific operations the runtime delegates to
6
+ //! (`encode_json`, `mark_raw`, `materialize`, `render_named`) -- adjusted
7
+ //! for the design contract's minijinja Environment shape (see the design
8
+ //! doc's "minijinja Environment contract" section, verified against
9
+ //! minijinja 2.21.0):
10
+ //!
11
+ //! * `ChainableUndefined` equivalent: [`UndefinedBehavior::Chainable`].
12
+ //! * `trim_blocks` / `lstrip_blocks`: both `true` (Jinja2 parity).
13
+ //! * `.j2` is NOT auto-escaped by minijinja's own extension-sniffing
14
+ //! default, so [`Environment::set_auto_escape_callback`] forces
15
+ //! `AutoEscape::Html` unconditionally.
16
+ //! * A custom [`Environment::set_formatter`] absorbs JS/minijinja
17
+ //! semantic differences UNIFORMLY (no per-fixture hacks): `undefined`/
18
+ //! `none` print nothing, safe values pass through raw, strings escape
19
+ //! with MarkupSafe-compatible entities (`&#39;` for `'`, NOT
20
+ //! minijinja's default `&#x27;` -- fixtures pin `&#39;`), numbers
21
+ //! format via [`crate::num::format_js_number`] (a fallback: templates
22
+ //! normally route through `bf.string()` first), bools print
23
+ //! `true`/`false`.
24
+ //!
25
+ //! `materialize` has no Rust analogue: JSX children are captured via
26
+ //! minijinja `{% set %}...{% endset %}` blocks, which always yield a
27
+ //! concrete rendered `Value::from_safe_string` (never a lazy callable, no
28
+ //! `bf-render` payload path constructs one either), so there is nothing to
29
+ //! resolve.
30
+
31
+ use crate::num::{self, JsValue};
32
+ use crate::runtime;
33
+ use minijinja::value::Value as MjValue;
34
+ use minijinja::{escape_formatter, path_loader, AutoEscape, Environment, Error, Output, State, UndefinedBehavior};
35
+ use std::collections::BTreeMap;
36
+ use std::path::Path;
37
+
38
+ /// Build the minijinja `Environment` per the design contract. `templates_dir`
39
+ /// contains `<snake_case>.j2` files loaded by base name (no `templates/`
40
+ /// prefix, no extension) via [`Environment::get_template`].
41
+ pub fn build_environment(templates_dir: &Path) -> Environment<'static> {
42
+ let mut env = Environment::new();
43
+ env.set_loader(path_loader(templates_dir));
44
+ env.set_undefined_behavior(UndefinedBehavior::Chainable);
45
+ env.set_trim_blocks(true);
46
+ env.set_lstrip_blocks(true);
47
+ env.set_auto_escape_callback(|_name| AutoEscape::Html);
48
+ env.set_formatter(bf_formatter);
49
+ env
50
+ }
51
+
52
+ /// MarkupSafe-compatible escape for plain (non-safe) string interpolation:
53
+ /// `&` `&amp;`, `<` `&lt;`, `>` `&gt;`, `"` `&quot;`, `'` `&#39;`. The ONLY
54
+ /// divergence from minijinja's own default HTML escaper
55
+ /// (`crate::utils::write_escaped` in the minijinja source) is the
56
+ /// apostrophe entity (minijinja default: `&#x27;`) -- everything else
57
+ /// matches minijinja's own table already, kept explicit here so the whole
58
+ /// policy lives in one place next to the formatter that uses it.
59
+ ///
60
+ /// NOT the same escaper as `runtime::html_escape` (used by `spread_attrs` /
61
+ /// `hydration_attrs` / `data_key_attr`'s raw attribute-syntax construction),
62
+ /// which uses `&#34;` for `"` (matching Go's `template.HTMLEscapeString`
63
+ /// byte-for-byte so SSR output stays identical across adapters). Two
64
+ /// call sites, two historically-pinned entity choices; unifying them is
65
+ /// out of scope for this port.
66
+ fn markupsafe_escape(s: &str) -> String {
67
+ let mut out = String::with_capacity(s.len());
68
+ for c in s.chars() {
69
+ match c {
70
+ '&' => out.push_str("&amp;"),
71
+ '<' => out.push_str("&lt;"),
72
+ '>' => out.push_str("&gt;"),
73
+ '"' => out.push_str("&quot;"),
74
+ '\'' => out.push_str("&#39;"),
75
+ _ => out.push(c),
76
+ }
77
+ }
78
+ out
79
+ }
80
+
81
+ fn bf_formatter(out: &mut Output, state: &State, value: &MjValue) -> Result<(), Error> {
82
+ if value.is_undefined() || value.is_none() {
83
+ return Ok(());
84
+ }
85
+ if value.is_safe() {
86
+ out.write_str(value.as_str().unwrap_or_default()).map_err(Error::from)?;
87
+ return Ok(());
88
+ }
89
+ match value.kind() {
90
+ minijinja::value::ValueKind::Bool => {
91
+ out.write_str(if value.is_true() { "true" } else { "false" }).map_err(Error::from)?;
92
+ }
93
+ minijinja::value::ValueKind::Number => {
94
+ let n = f64::try_from(value.clone()).unwrap_or(f64::NAN);
95
+ out.write_str(&num::format_js_number(n)).map_err(Error::from)?;
96
+ }
97
+ minijinja::value::ValueKind::String => {
98
+ out.write_str(&markupsafe_escape(value.as_str().unwrap_or(""))).map_err(Error::from)?;
99
+ }
100
+ // Arrays/maps/objects are never interpolated directly by generated
101
+ // templates (they always go through `bf.json` / `bf.string` /
102
+ // `bf.spread_attrs` first) -- fall back to minijinja's own default
103
+ // rendering for defensive completeness.
104
+ _ => escape_formatter(out, state, value)?,
105
+ }
106
+ Ok(())
107
+ }
108
+
109
+ /// Canonical JSON encoding for `bf.json` / `bf-p` / `bf-scope`: sorted keys
110
+ /// (free, since [`JsValue::Object`] is a `BTreeMap`), compact separators
111
+ /// (`serde_json::to_string`'s default), non-finite floats -> JSON `null`
112
+ /// recursively (`JsValue::to_json`'s documented contract -- see `num.rs`).
113
+ pub fn encode_json(v: &JsValue) -> String {
114
+ serde_json::to_string(&v.to_json()).expect("JsValue::to_json always produces a JSON-encodable document")
115
+ }
116
+
117
+ fn build_context(bf_value: MjValue, vars: &JsValue) -> BTreeMap<String, MjValue> {
118
+ let mut ctx = BTreeMap::new();
119
+ if let Some(map) = vars.as_object() {
120
+ for (k, v) in map {
121
+ ctx.insert(runtime::mangle_ident(k), runtime::js_to_mj(v));
122
+ }
123
+ }
124
+ ctx.insert("bf".to_string(), bf_value);
125
+ ctx
126
+ }
127
+
128
+ /// Render `<name>.j2` with `bf_value` bound as the `bf` variable, plus the
129
+ /// supplied template vars (keyword-mangled -- the one point every props
130
+ /// dict is turned into template variables). Used for the top-level entry
131
+ /// render (`bf-render`'s own `env`, no template is executing yet).
132
+ pub fn render_named(env: &Environment<'static>, template_name: &str, bf_value: MjValue, vars: &JsValue) -> Result<String, Error> {
133
+ let template = env.get_template(&format!("{template_name}.j2"))?;
134
+ template.render(build_context(bf_value, vars))
135
+ }
136
+
137
+ /// Same as [`render_named`] but sourced from a `State` -- the re-entrant
138
+ /// path `bf.render_child` uses to render a child template from WITHIN an
139
+ /// already-executing template, via `state.env()`.
140
+ pub fn render_named_from_state(state: &State, template_name: &str, bf_value: MjValue, vars: &JsValue) -> Result<String, Error> {
141
+ let template = state.env().get_template(&format!("{template_name}.j2"))?;
142
+ template.render(build_context(bf_value, vars))
143
+ }
144
+
145
+ /// Like [`render_named_from_state`], but `vars` are ALREADY
146
+ /// `minijinja::Value`s with ALREADY keyword-mangled keys (built by
147
+ /// `render_child`), rather than a `JsValue` document to mangle-and-convert
148
+ /// here. This is the child-render path: `render_child` must keep props as
149
+ /// `Value` end-to-end so a safe (JSX children) value survives -- routing
150
+ /// through `&JsValue`/[`build_context`] would strip the safe flag off any
151
+ /// safe-string entry (see `runtime::BfInstance::render_child`'s docstring).
152
+ pub fn render_named_from_state_values(
153
+ state: &State,
154
+ template_name: &str,
155
+ bf_value: MjValue,
156
+ mut vars: BTreeMap<String, MjValue>,
157
+ ) -> Result<String, Error> {
158
+ let template = state.env().get_template(&format!("{template_name}.j2"))?;
159
+ vars.insert("bf".to_string(), bf_value);
160
+ template.render(vars)
161
+ }
162
+
163
+ /// Like [`render_named`], plus extra raw `minijinja::Value` context entries
164
+ /// (keyword-mangled like every other var) inserted after the `vars`-derived
165
+ /// ones. Used by `bf-render`'s entry point to bind `searchParams` -- a
166
+ /// [`crate::search_params::SearchParams`] `Object`, which has no
167
+ /// [`JsValue`] representation (it isn't a JSON-shaped value), so it can't
168
+ /// flow through the normal `vars: &JsValue` path.
169
+ pub fn render_entry(env: &Environment<'static>, template_name: &str, bf_value: MjValue, vars: &JsValue, extra_vars: &[(String, MjValue)]) -> Result<String, Error> {
170
+ let template = env.get_template(&format!("{template_name}.j2"))?;
171
+ let mut ctx = build_context(bf_value, vars);
172
+ for (k, v) in extra_vars {
173
+ ctx.insert(runtime::mangle_ident(k), v.clone());
174
+ }
175
+ template.render(ctx)
176
+ }
@@ -0,0 +1,147 @@
1
+ //! Conformance renderer binary: `bf-render <path-to-payload.json>`.
2
+ //!
3
+ //! Implements the bf-render payload protocol (design doc, "bf-render
4
+ //! payload protocol" section) verbatim. HTML -> stdout on success; on
5
+ //! error, a message -> stderr and exit 1.
6
+ //!
7
+ //! Non-finite numbers (JSON cannot spell NaN/Infinity) travel as
8
+ //! `{"__bf_special": "nan" | "inf" | "-inf"}` sentinel objects, decoded
9
+ //! recursively AFTER `serde_json` parses the document (the TS side
10
+ //! transforms values recursively BEFORE `JSON.stringify`; this binary is
11
+ //! the mirror-image decode step).
12
+
13
+ use barefootjs::backend_minijinja;
14
+ use barefootjs::num::JsValue;
15
+ use barefootjs::runtime::{js_to_mj, BfInstance, ChildRendererSpec, RenderSession};
16
+ use barefootjs::search_params::SearchParams;
17
+ use minijinja::value::Value as MjValue;
18
+ use serde::Deserialize;
19
+ use serde_json::Value as JsonValue;
20
+ use std::path::PathBuf;
21
+ use std::process::ExitCode;
22
+ use std::sync::Arc;
23
+
24
+ #[derive(Deserialize)]
25
+ struct ChildPayload {
26
+ name: String,
27
+ template: String,
28
+ #[serde(default)]
29
+ ssr_defaults: JsonValue,
30
+ #[serde(default)]
31
+ rest_props_name: Option<String>,
32
+ #[serde(default)]
33
+ param_names: Vec<String>,
34
+ }
35
+
36
+ #[derive(Deserialize)]
37
+ struct Payload {
38
+ templates_dir: String,
39
+ entry: String,
40
+ scope_id: String,
41
+ #[serde(default)]
42
+ vars: JsonValue,
43
+ #[serde(default)]
44
+ search_params: Option<String>,
45
+ #[serde(default)]
46
+ children: Vec<ChildPayload>,
47
+ }
48
+
49
+ /// Recursively decode a parsed JSON document into the working [`JsValue`]
50
+ /// domain, resolving `{"__bf_special": "nan" | "inf" | "-inf"}` sentinel
51
+ /// objects into real non-finite `f64`s along the way -- these are the ONE
52
+ /// shape `serde_json::Value` cannot carry natively (see `num.rs`'s module
53
+ /// docstring), so decoding happens here rather than via
54
+ /// `JsValue::from_json` (which has no sentinel to look for -- ordinary
55
+ /// JSON documents never need one).
56
+ fn decode_value(v: &JsonValue) -> JsValue {
57
+ if let JsonValue::Object(map) = v {
58
+ if map.len() == 1 {
59
+ if let Some(JsonValue::String(kind)) = map.get("__bf_special") {
60
+ let n = match kind.as_str() {
61
+ "nan" => f64::NAN,
62
+ "inf" => f64::INFINITY,
63
+ "-inf" => f64::NEG_INFINITY,
64
+ _ => f64::NAN,
65
+ };
66
+ return JsValue::Number(n);
67
+ }
68
+ }
69
+ return JsValue::Object(map.iter().map(|(k, v)| (k.clone(), decode_value(v))).collect());
70
+ }
71
+ match v {
72
+ JsonValue::Null => JsValue::Null,
73
+ JsonValue::Bool(b) => JsValue::Bool(*b),
74
+ JsonValue::Number(n) => JsValue::Number(n.as_f64().unwrap_or(f64::NAN)),
75
+ JsonValue::String(s) => JsValue::String(s.clone()),
76
+ JsonValue::Array(a) => JsValue::Array(a.iter().map(decode_value).collect()),
77
+ JsonValue::Object(_) => unreachable!("handled above"),
78
+ }
79
+ }
80
+
81
+ fn run(payload_path: &str) -> Result<String, String> {
82
+ let text = std::fs::read_to_string(payload_path).map_err(|e| format!("failed to read {payload_path}: {e}"))?;
83
+ let payload: Payload = serde_json::from_str(&text).map_err(|e| format!("invalid payload JSON: {e}"))?;
84
+
85
+ let templates_dir = PathBuf::from(&payload.templates_dir);
86
+ let env = backend_minijinja::build_environment(&templates_dir);
87
+
88
+ let session = RenderSession::new();
89
+ for child in &payload.children {
90
+ session.register_child_renderer(
91
+ // Keyed by the snake_case template base name -- matches the
92
+ // literal `bf.render_child('<snake_name>', ...)` call sites the
93
+ // TS emitter generates (`toTemplateName(comp.name)`), mirroring
94
+ // `buildChildRenderers`'s `bf.register_child_renderer(snakeName, ...)`.
95
+ child.template.clone(),
96
+ ChildRendererSpec {
97
+ component_name: child.name.clone(),
98
+ template: child.template.clone(),
99
+ // Converted from JSON to `Value` HERE, at registration time
100
+ // -- `render_child` keeps props as `Value` end-to-end from
101
+ // this point on (see `ChildRendererSpec::ssr_defaults`'s
102
+ // docstring).
103
+ ssr_defaults: js_to_mj(&decode_value(&child.ssr_defaults)),
104
+ rest_props_name: child.rest_props_name.clone(),
105
+ param_names: child.param_names.clone(),
106
+ },
107
+ );
108
+ }
109
+
110
+ let root = BfInstance::root(Arc::clone(&session), payload.scope_id.clone());
111
+ let vars = decode_value(&payload.vars);
112
+
113
+ let mut extra: Vec<(String, MjValue)> = Vec::new();
114
+ if let Some(query) = &payload.search_params {
115
+ extra.push(("searchParams".to_string(), SearchParams::new(query).to_value()));
116
+ }
117
+
118
+ backend_minijinja::render_entry(&env, &payload.entry, root.as_mj_value(), &vars, &extra).map_err(|e| {
119
+ let mut msg = format!("{e:#}");
120
+ let mut source = std::error::Error::source(&e);
121
+ while let Some(s) = source {
122
+ msg.push_str(&format!("\ncaused by: {s}"));
123
+ source = s.source();
124
+ }
125
+ msg
126
+ })
127
+ }
128
+
129
+ fn main() -> ExitCode {
130
+ let payload_path = match std::env::args().nth(1) {
131
+ Some(p) => p,
132
+ None => {
133
+ eprintln!("usage: bf-render <path-to-payload.json>");
134
+ return ExitCode::FAILURE;
135
+ }
136
+ };
137
+ match run(&payload_path) {
138
+ Ok(html) => {
139
+ print!("{html}");
140
+ ExitCode::SUCCESS
141
+ }
142
+ Err(msg) => {
143
+ eprintln!("{msg}");
144
+ ExitCode::FAILURE
145
+ }
146
+ }
147
+ }