@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.
- package/README.md +194 -0
- package/dist/adapter/analysis/component-tree.d.ts +26 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/boolean-result.d.ts +85 -0
- package/dist/adapter/boolean-result.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +107 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/array-method.d.ts +75 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +143 -0
- package/dist/adapter/expr/emitters.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +6 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +189091 -0
- package/dist/adapter/lib/constants.d.ts +25 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +50 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/minijinja-naming.d.ts +64 -0
- package/dist/adapter/lib/minijinja-naming.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +32 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/seed.d.ts +84 -0
- package/dist/adapter/memo/seed.d.ts.map +1 -0
- package/dist/adapter/minijinja-adapter.d.ts +421 -0
- package/dist/adapter/minijinja-adapter.d.ts.map +1 -0
- package/dist/adapter/props/prop-classes.d.ts +33 -0
- package/dist/adapter/props/prop-classes.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +63 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal.d.ts +28 -0
- package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
- package/dist/build.d.ts +29 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +189111 -0
- package/dist/conformance-pins.d.ts +13 -0
- package/dist/conformance-pins.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +189112 -0
- package/package.json +67 -0
- package/runtime/Cargo.lock +124 -0
- package/runtime/Cargo.toml +21 -0
- package/runtime/src/backend_minijinja.rs +176 -0
- package/runtime/src/bin/bf-render.rs +147 -0
- package/runtime/src/evaluator.rs +770 -0
- package/runtime/src/lib.rs +19 -0
- package/runtime/src/manifest.rs +258 -0
- package/runtime/src/num.rs +558 -0
- package/runtime/src/runtime.rs +1548 -0
- package/runtime/src/search_params.rs +173 -0
- package/runtime/tests/eval_vectors.rs +94 -0
- package/runtime/tests/evaluator.rs +407 -0
- package/runtime/tests/helper_vectors.rs +348 -0
- package/runtime/tests/manifest.rs +169 -0
- package/runtime/tests/omit.rs +79 -0
- package/runtime/tests/props_attr.rs +75 -0
- package/runtime/tests/query.rs +50 -0
- package/runtime/tests/render_child.rs +210 -0
- package/runtime/tests/search_params.rs +68 -0
- package/runtime/tests/spread_attrs.rs +94 -0
- package/runtime/tests/template_primitives.rs +376 -0
- package/runtime/tests/vector-divergences.json +33 -0
- package/src/__tests__/minijinja-adapter-unit.test.ts +392 -0
- package/src/__tests__/minijinja-adapter.test.ts +58 -0
- package/src/__tests__/minijinja-counter.test.ts +61 -0
- package/src/__tests__/minijinja-query-href.test.ts +101 -0
- package/src/__tests__/minijinja-spread-attrs.test.ts +227 -0
- package/src/adapter/analysis/component-tree.ts +119 -0
- package/src/adapter/boolean-result.ts +177 -0
- package/src/adapter/emit-context.ts +119 -0
- package/src/adapter/expr/array-method.ts +346 -0
- package/src/adapter/expr/emitters.ts +608 -0
- package/src/adapter/index.ts +6 -0
- package/src/adapter/lib/constants.ts +37 -0
- package/src/adapter/lib/ir-scope.ts +95 -0
- package/src/adapter/lib/minijinja-naming.ts +85 -0
- package/src/adapter/lib/types.ts +35 -0
- package/src/adapter/memo/seed.ts +135 -0
- package/src/adapter/minijinja-adapter.ts +1796 -0
- package/src/adapter/props/prop-classes.ts +65 -0
- package/src/adapter/spread/spread-codegen.ts +168 -0
- package/src/adapter/value/parsed-literal.ts +76 -0
- package/src/build.ts +38 -0
- package/src/conformance-pins.ts +101 -0
- package/src/index.ts +12 -0
- 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(®istry_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
|
+
}
|