@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
@@ -0,0 +1,19 @@
1
+ //! `barefootjs` -- Rust runtime for BarefootJS marked templates (the
2
+ //! minijinja adapter). Port of
3
+ //! `packages/adapter-jinja/python/barefootjs/` (itself a port of
4
+ //! `packages/adapter-perl/lib/BarefootJS.pm`), consulting
5
+ //! `packages/adapter-go-template/runtime/*.go` where Rust-shape questions
6
+ //! arose. See each module's docstring for its specific provenance and any
7
+ //! documented divergences.
8
+
9
+ pub mod backend_minijinja;
10
+ pub mod evaluator;
11
+ pub mod manifest;
12
+ pub mod num;
13
+ pub mod runtime;
14
+ pub mod search_params;
15
+
16
+ pub use manifest::{derive_stash_from_defaults, load_manifest, register_components_from_manifest, to_template_name};
17
+ pub use num::JsValue;
18
+ pub use runtime::{BfInstance, ChildRendererSpec, RenderSession};
19
+ pub use search_params::SearchParams;
@@ -0,0 +1,258 @@
1
+ //! Production manifest-driven child-component registration -- Rust port of
2
+ //! `packages/adapter-jinja/python/barefootjs/runtime.py`'s
3
+ //! `register_components_from_manifest` / `_derive_stash_from_defaults`, the
4
+ //! path a production host (the `integrations/axum` example) uses to
5
+ //! register every compiled component from the `bf build` manifest
6
+ //! (`manifest.json`) in one call instead of hand-wiring
7
+ //! `register_child_renderer` per route the way `integrations/flask/app.py`
8
+ //! does.
9
+ //!
10
+ //! ## Divergence from the Python port
11
+ //!
12
+ //! Python's `register_components_from_manifest` (`runtime.py` lines
13
+ //! 621-706) only registers manifest entries shaped `ui/<name>/index`
14
+ //! (`_MANIFEST_ENTRY_RE`) -- a component-library registry convention for
15
+ //! the separate `/ui` design-system package. This port additionally
16
+ //! accepts FLAT entries (`"PostListItem"`, `"ToggleItem"`, ...), which is
17
+ //! what EVERY entry in a `bf build` manifest looks like for a plain
18
+ //! `components: [...]` config with no `ui/` subfolder (verified against a
19
+ //! real build of `integrations/flask`: `dist/templates/manifest.json` has
20
+ //! no `ui/`-prefixed keys at all -- see `packages/cli/src/lib/build.ts`'s
21
+ //! `effectiveNamesFor`). `integrations/axum`'s own manifest is entirely
22
+ //! flat (`components: ['../shared/components', '../shared/blog']`, same
23
+ //! layout as flask/gin), so a literal `ui/`-only port would register
24
+ //! nothing there -- this extension is what makes the function "the
25
+ //! production path to register every compiled component" the axum
26
+ //! integration needs. `ui/<name>/index` entries are still recognised
27
+ //! exactly like the Python port, so a `ui/` component-library registry
28
+ //! works unchanged if one is ever compiled into an integration's manifest.
29
+ //!
30
+ //! Unlike Python (which builds a bespoke renderer closure per manifest
31
+ //! entry, re-deriving scope-id chaining / rest-bag routing / ssrDefaults
32
+ //! merge inline in `make_renderer` every time), this port only needs to
33
+ //! REGISTER a [`ChildRendererSpec`] per entry: [`crate::runtime::BfInstance::
34
+ //! render_child`] already implements that whole contract generically for
35
+ //! every registered child (scope-id chaining, `_bf_slot`/`key` popping,
36
+ //! rest-bag routing, ssrDefaults-then-caller-props merge -- see its
37
+ //! docstring), because it was built to serve `bf-render`'s payload-driven
38
+ //! children the same way. Registering a manifest entry is therefore just:
39
+ //! derive the registry key + template name from `markedTemplate`, flatten
40
+ //! `ssrDefaults` to its static fallback values via
41
+ //! [`derive_stash_from_defaults`] (called with EMPTY props, since the
42
+ //! caller-props override Python's closure applies per-call is already
43
+ //! applied generically, once, inside `render_child`), and derive
44
+ //! `rest_props_name`/`param_names` from which `ssrDefaults` entries carry
45
+ //! `isRestProps`/`propName` (see `packages/jsx/src/ssr-defaults.ts`'s
46
+ //! `extractSsrDefaults`, which only ever sets `propName` to the entry's OWN
47
+ //! key -- so this static-then-override merge order is exactly equivalent
48
+ //! to Python's per-call `props.get(propName, value)` derivation).
49
+
50
+ use crate::num::JsValue;
51
+ use crate::runtime::{js_to_mj, ChildRendererSpec, RenderSession};
52
+ use std::collections::{BTreeMap, HashMap};
53
+ use std::sync::Arc;
54
+
55
+ /// Convert a PascalCase component name to the snake_case template/registry
56
+ /// name every compiled template's `bf.render_child('<snake>', ...)` call
57
+ /// site uses. Port of the TS emitter's naming transform (byte-identical
58
+ /// logic to `packages/adapter-rust/src/adapter/minijinja-adapter.ts`'s
59
+ /// `toTemplateName` and `packages/adapter-rust/src/test-render.ts`'s
60
+ /// `toSnakeCase`; verified against a real build's `render_child('toggle_
61
+ /// item', ...)` / `render_child('post_list_item', ...)` call sites) --
62
+ /// kept in lock-step with those two.
63
+ pub fn to_template_name(component_name: &str) -> String {
64
+ let mut out = String::with_capacity(component_name.len() + 4);
65
+ for (i, c) in component_name.chars().enumerate() {
66
+ if c.is_ascii_uppercase() {
67
+ if i > 0 {
68
+ out.push('_');
69
+ }
70
+ out.push(c.to_ascii_lowercase());
71
+ } else {
72
+ out.push(c);
73
+ }
74
+ }
75
+ out
76
+ }
77
+
78
+ /// Derive template-stash key/value pairs from a manifest entry's
79
+ /// `ssrDefaults` section (port of `runtime.py`'s
80
+ /// `_derive_stash_from_defaults`). Each entry is either a bare JSON value
81
+ /// (used as-is) or an object shaped `{value, propName?, isRestProps?}`:
82
+ ///
83
+ /// * `isRestProps: true` -- prefer `props[<this entry's own key>]` (the
84
+ /// rest-props bag the caller may have already assembled), else the
85
+ /// static `value` fallback (normally `{}`).
86
+ /// * `propName` set -- prefer `props[propName]` when present AND not
87
+ /// `null`/`undefined`, else the static `value` fallback. Every
88
+ /// `propName` the TS extractor emits equals its own entry's key (see
89
+ /// `extractSsrDefaults`), so this always reads back the SAME key it
90
+ /// writes -- a caller with no relevant props (this module's own
91
+ /// registration path, below) can pass an empty `props` document to get
92
+ /// pure static fallbacks.
93
+ /// * neither set -- the static `value` (a signal/memo's default,
94
+ /// internal to the component, never sourced from `props`).
95
+ pub fn derive_stash_from_defaults(defaults: &JsValue, props: &JsValue) -> JsValue {
96
+ let defaults_map = match defaults.as_object() {
97
+ Some(m) => m,
98
+ None => return JsValue::Object(BTreeMap::new()),
99
+ };
100
+ let props_map = props.as_object();
101
+ let mut extra = BTreeMap::new();
102
+ for (name, d) in defaults_map {
103
+ let dm = match d.as_object() {
104
+ Some(m) => m,
105
+ None => {
106
+ extra.insert(name.clone(), d.clone());
107
+ continue;
108
+ }
109
+ };
110
+ let fallback = || dm.get("value").cloned().unwrap_or(JsValue::Null);
111
+ if matches!(dm.get("isRestProps"), Some(JsValue::Bool(true))) {
112
+ let v = props_map.and_then(|p| p.get(name)).cloned().unwrap_or_else(fallback);
113
+ extra.insert(name.clone(), v);
114
+ continue;
115
+ }
116
+ let prop_name = dm.get("propName").and_then(|v| v.as_str());
117
+ let from_props = prop_name.and_then(|pn| props_map.and_then(|p| p.get(pn)));
118
+ let v = match from_props {
119
+ Some(pv) if !matches!(pv, JsValue::Null) => pv.clone(),
120
+ _ => fallback(),
121
+ };
122
+ extra.insert(name.clone(), v);
123
+ }
124
+ JsValue::Object(extra)
125
+ }
126
+
127
+ /// Strip the manifest's `markedTemplate` value (e.g. `"templates/PostList
128
+ /// Item.j2"`) down to the bare template base name (`"PostListItem"`) --
129
+ /// mirrors `runtime.py`'s `_STRIPPED_TEMPLATE_SUFFIXES` handling, extended
130
+ /// with `.j2` (this adapter's own extension; the Python list only needed
131
+ /// `.html.ep` / `.tx` / `.jinja` for its own sibling adapters).
132
+ fn strip_manifest_template_path(marked: &str) -> String {
133
+ let stripped = marked.strip_prefix("templates/").unwrap_or(marked);
134
+ for suffix in [".j2", ".jinja", ".html.ep", ".tx"] {
135
+ if let Some(s) = stripped.strip_suffix(suffix) {
136
+ return s.to_string();
137
+ }
138
+ }
139
+ stripped.to_string()
140
+ }
141
+
142
+ /// `ui/<name>/index` -> `<name>` (Python's `_MANIFEST_ENTRY_RE`); any other
143
+ /// manifest key with no `/` is used verbatim as the PascalCase component
144
+ /// name (this crate's flat-manifest extension -- see the module
145
+ /// docstring). A key with a `/` that isn't `ui/<name>/index`-shaped is an
146
+ /// unrecognised nested path shape and is skipped rather than guessed at.
147
+ fn component_name_for_entry(entry_name: &str) -> Option<String> {
148
+ if let Some(rest) = entry_name.strip_prefix("ui/") {
149
+ return rest.strip_suffix("/index").map(str::to_string);
150
+ }
151
+ if entry_name.contains('/') {
152
+ return None;
153
+ }
154
+ Some(entry_name.to_string())
155
+ }
156
+
157
+ /// Walk a `bf build` manifest (`manifest.json`, decoded to [`JsValue`] via
158
+ /// [`JsValue::from_json`]) and register one [`ChildRendererSpec`] per
159
+ /// component entry into `session`, so every compiled component (anything a
160
+ /// template reaches via `bf.render_child(...)`) is reachable in a single
161
+ /// call -- the production registration path `integrations/axum`'s route
162
+ /// handlers use (see the module docstring's divergence note for why this
163
+ /// needn't re-derive `render_child`'s per-call logic the way Python's
164
+ /// closures do).
165
+ ///
166
+ /// `signal_init` is an opt-in STATIC override keyed by the REGISTRY key
167
+ /// (the same snake_case name `render_child` looks up, e.g.
168
+ /// `"post_list_item"`): entries here are merged OVER the manifest-derived
169
+ /// defaults at registration time. This is the Rust-appropriate shape of
170
+ /// Python's per-call `signal_init` callback (`props -> dict`): since
171
+ /// `render_child` already applies the CALLER's per-call props on top of
172
+ /// whatever is registered here (see its docstring), a static override map
173
+ /// covers every case a registered CHILD needs. A derivation that genuinely
174
+ /// depends on the current REQUEST (not the immediate caller's props --
175
+ /// e.g. the blog's `PostList` reading `?sort=`/`?tag=` from the URL query)
176
+ /// is a ROOT-level render, not a registered child, and is handled by
177
+ /// calling [`derive_stash_from_defaults`] directly with the real
178
+ /// request-derived props instead (see `integrations/axum/src/blog.rs`).
179
+ pub fn register_components_from_manifest(
180
+ session: &Arc<RenderSession>,
181
+ manifest: &JsValue,
182
+ signal_init: &HashMap<String, JsValue>,
183
+ ) {
184
+ let entries = match manifest.as_object() {
185
+ Some(m) => m,
186
+ None => return,
187
+ };
188
+ for (entry_name, entry) in entries {
189
+ if entry_name == "__barefoot__" {
190
+ continue;
191
+ }
192
+ let component_name = match component_name_for_entry(entry_name) {
193
+ Some(n) => n,
194
+ None => continue,
195
+ };
196
+ let entry_obj = match entry.as_object() {
197
+ Some(m) => m,
198
+ None => continue,
199
+ };
200
+ let marked = entry_obj.get("markedTemplate").and_then(|v| v.as_str()).unwrap_or("");
201
+ if marked.is_empty() {
202
+ continue;
203
+ }
204
+ let template_name = strip_manifest_template_path(marked);
205
+ let registry_key = to_template_name(&component_name);
206
+
207
+ let empty_props = JsValue::Object(BTreeMap::new());
208
+ let ssr_defaults = entry_obj.get("ssrDefaults").cloned().unwrap_or_else(|| JsValue::Object(BTreeMap::new()));
209
+ let mut defaults = derive_stash_from_defaults(&ssr_defaults, &empty_props);
210
+ if let Some(overrides) = signal_init.get(&registry_key) {
211
+ if let (JsValue::Object(base), Some(over)) = (&mut defaults, overrides.as_object()) {
212
+ for (k, v) in over {
213
+ base.insert(k.clone(), v.clone());
214
+ }
215
+ }
216
+ }
217
+
218
+ let (rest_props_name, param_names) = match ssr_defaults.as_object() {
219
+ Some(m) => {
220
+ let mut rest = None;
221
+ let mut params = Vec::new();
222
+ for (name, d) in m {
223
+ let Some(dm) = d.as_object() else { continue };
224
+ if matches!(dm.get("isRestProps"), Some(JsValue::Bool(true))) {
225
+ rest = Some(name.clone());
226
+ } else if dm.contains_key("propName") {
227
+ params.push(name.clone());
228
+ }
229
+ }
230
+ (rest, params)
231
+ }
232
+ None => (None, Vec::new()),
233
+ };
234
+
235
+ session.register_child_renderer(
236
+ registry_key,
237
+ ChildRendererSpec {
238
+ component_name: template_name.clone(),
239
+ template: template_name,
240
+ ssr_defaults: js_to_mj(&defaults),
241
+ rest_props_name,
242
+ param_names,
243
+ },
244
+ );
245
+ }
246
+ }
247
+
248
+ /// Read and decode a `bf build` manifest.json from disk into the working
249
+ /// [`JsValue`] domain -- the one bit of I/O plumbing every production host
250
+ /// needs before it can call [`register_components_from_manifest`], kept
251
+ /// here so `integrations/axum` doesn't hand-roll the same three lines
252
+ /// (`fs::read_to_string` + `serde_json::from_str` + `JsValue::from_json`).
253
+ pub fn load_manifest(path: &std::path::Path) -> std::io::Result<JsValue> {
254
+ let text = std::fs::read_to_string(path)?;
255
+ let parsed: serde_json::Value =
256
+ serde_json::from_str(&text).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
257
+ Ok(JsValue::from_json(&parsed))
258
+ }