@barefootjs/rust 0.31.1 → 0.31.3
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/dist/index.js +1 -4
- package/dist/render-divergences.d.ts.map +1 -1
- package/dist/vite.js +415 -100
- package/package.json +5 -5
- package/runtime/src/bin/bf-render.rs +7 -6
- package/runtime/src/manifest.rs +76 -27
- package/runtime/src/runtime.rs +90 -12
- package/runtime/tests/manifest.rs +35 -0
- package/runtime/tests/render_child.rs +41 -7
- package/src/render-divergences.ts +0 -16
- package/src/test-render.ts +45 -40
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/rust",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.3",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"directory": "packages/adapter-rust"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@barefootjs/shared": "0.31.
|
|
57
|
+
"@barefootjs/shared": "0.31.3"
|
|
58
58
|
},
|
|
59
59
|
"peerDependencies": {
|
|
60
60
|
"@barefootjs/jsx": ">=0.2.0",
|
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
},
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@barefootjs/adapter-tests": "0.1.0",
|
|
74
|
-
"@barefootjs/jsx": "0.31.
|
|
75
|
-
"@barefootjs/vite": "0.31.
|
|
76
|
-
"@barefootjs/client": "0.31.
|
|
74
|
+
"@barefootjs/jsx": "0.31.3",
|
|
75
|
+
"@barefootjs/vite": "0.31.3",
|
|
76
|
+
"@barefootjs/client": "0.31.3",
|
|
77
77
|
"typescript": "^5.0.0",
|
|
78
78
|
"vite": "^6.0.0"
|
|
79
79
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
use barefootjs::backend_minijinja;
|
|
14
14
|
use barefootjs::num::JsValue;
|
|
15
|
-
use barefootjs::runtime::{
|
|
15
|
+
use barefootjs::runtime::{BfInstance, ChildRendererSpec, RenderSession};
|
|
16
16
|
use barefootjs::search_params::SearchParams;
|
|
17
17
|
use minijinja::value::Value as MjValue;
|
|
18
18
|
use serde::Deserialize;
|
|
@@ -96,11 +96,12 @@ fn run(payload_path: &str) -> Result<String, String> {
|
|
|
96
96
|
ChildRendererSpec {
|
|
97
97
|
component_name: child.name.clone(),
|
|
98
98
|
template: child.template.clone(),
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
ssr_defaults
|
|
99
|
+
// Kept RAW (`JsValue`, the FULL `{value, propName?,
|
|
100
|
+
// isRestProps?}` shape) -- `render_child` resolves this
|
|
101
|
+
// PER-CALL against the real caller props via
|
|
102
|
+
// `derive_stash_from_defaults` (see `ChildRendererSpec::
|
|
103
|
+
// ssr_defaults`'s docstring; #2524).
|
|
104
|
+
ssr_defaults: decode_value(&child.ssr_defaults),
|
|
104
105
|
rest_props_name: child.rest_props_name.clone(),
|
|
105
106
|
param_names: child.param_names.clone(),
|
|
106
107
|
},
|
package/runtime/src/manifest.rs
CHANGED
|
@@ -33,22 +33,34 @@
|
|
|
33
33
|
//! REGISTER a [`ChildRendererSpec`] per entry: [`crate::runtime::BfInstance::
|
|
34
34
|
//! render_child`] already implements that whole contract generically for
|
|
35
35
|
//! every registered child (scope-id chaining, `_bf_slot`/`key` popping,
|
|
36
|
-
//! rest-bag routing,
|
|
37
|
-
//! docstring), because it was built to serve `bf-render`'s
|
|
38
|
-
//! children the same way. Registering a manifest entry is
|
|
39
|
-
//! derive the registry key + template name from
|
|
40
|
-
//! `ssrDefaults`
|
|
41
|
-
//!
|
|
42
|
-
//!
|
|
43
|
-
//!
|
|
44
|
-
//!
|
|
45
|
-
//! `
|
|
46
|
-
//! `
|
|
47
|
-
//!
|
|
48
|
-
//!
|
|
36
|
+
//! rest-bag routing, `derive_stash_from_defaults`-then-caller-props merge --
|
|
37
|
+
//! see its docstring), because it was built to serve `bf-render`'s
|
|
38
|
+
//! payload-driven children the same way. Registering a manifest entry is
|
|
39
|
+
//! therefore just: derive the registry key + template name from
|
|
40
|
+
//! `markedTemplate`, keep `ssrDefaults` RAW (the FULL `{value, propName?,
|
|
41
|
+
//! isRestProps?}` shape, with any STATIC `signal_init` override applied as
|
|
42
|
+
//! a bare replacement value), and derive `rest_props_name`/`param_names`
|
|
43
|
+
//! from which `ssrDefaults` entries carry `isRestProps`/`propName`.
|
|
44
|
+
//!
|
|
45
|
+
//! `ssrDefaults` is stored RAW, not flattened, and resolved PER-CALL inside
|
|
46
|
+
//! `render_child` against the caller's ACTUAL props (#2524). An earlier
|
|
47
|
+
//! version of this function flattened via [`derive_stash_from_defaults`]
|
|
48
|
+
//! called with an EMPTY props document AT REGISTRATION TIME -- on the
|
|
49
|
+
//! (false) assumption that every `propName` equals its own entry's key, so
|
|
50
|
+
//! resolving early against no props and resolving later against the real
|
|
51
|
+
//! caller props would agree. That assumption breaks for an ALIASED
|
|
52
|
+
//! destructured prop (`{ n: count }`, see `packages/jsx/src/ssr-defaults.ts`'s
|
|
53
|
+
//! `extractSsrDefaults`): the entry's own key is the LOCAL binding
|
|
54
|
+
//! (`count`), but `propName` is the CALLER-facing name (`n`) -- resolving
|
|
55
|
+
//! against an empty props document can only ever produce the static
|
|
56
|
+
//! fallback, silently discarding whatever the caller actually passed as
|
|
57
|
+
//! `n`. `param_names` has the matching fix: it carries the CALLER-facing
|
|
58
|
+
//! `propName`, not the entry's own (local) key, since `render_child`'s
|
|
59
|
+
//! rest-bag "keep" set compares against child props keyed by whatever the
|
|
60
|
+
//! calling template passed.
|
|
49
61
|
|
|
50
62
|
use crate::num::JsValue;
|
|
51
|
-
use crate::runtime::{
|
|
63
|
+
use crate::runtime::{ChildRendererSpec, RenderSession};
|
|
52
64
|
use std::collections::{BTreeMap, HashMap};
|
|
53
65
|
use std::sync::Arc;
|
|
54
66
|
|
|
@@ -84,12 +96,17 @@ pub fn to_template_name(component_name: &str) -> String {
|
|
|
84
96
|
/// rest-props bag the caller may have already assembled), else the
|
|
85
97
|
/// static `value` fallback (normally `{}`).
|
|
86
98
|
/// * `propName` set -- prefer `props[propName]` when present AND not
|
|
87
|
-
/// `null`/`undefined`, else the static `value` fallback.
|
|
88
|
-
/// `propName`
|
|
89
|
-
///
|
|
90
|
-
///
|
|
91
|
-
///
|
|
92
|
-
///
|
|
99
|
+
/// `null`/`undefined`, else the static `value` fallback. For an
|
|
100
|
+
/// UN-aliased prop `propName` equals the entry's own key; for an
|
|
101
|
+
/// ALIASED destructured prop (`{ n: count }`, see
|
|
102
|
+
/// `packages/jsx/src/ssr-defaults.ts`'s `extractSsrDefaults`) the
|
|
103
|
+
/// entry's key is the LOCAL binding (`count`) but `propName` is the
|
|
104
|
+
/// CALLER-facing name (`n`) -- callers MUST pass the real caller props
|
|
105
|
+
/// (not an empty document) for this branch to do anything useful (see
|
|
106
|
+
/// `render_child`, which calls this per-call against the actual caller
|
|
107
|
+
/// props; this module's own registration path keeps `ssrDefaults` RAW
|
|
108
|
+
/// and defers to that per-call resolution rather than resolving early
|
|
109
|
+
/// against an empty `props` document -- #2524).
|
|
93
110
|
/// * neither set -- the static `value` (a signal/memo's default,
|
|
94
111
|
/// internal to the component, never sourced from `props`).
|
|
95
112
|
pub fn derive_stash_from_defaults(defaults: &JsValue, props: &JsValue) -> JsValue {
|
|
@@ -204,17 +221,49 @@ pub fn register_components_from_manifest(
|
|
|
204
221
|
let template_name = strip_manifest_template_path(marked);
|
|
205
222
|
let registry_key = to_template_name(&component_name);
|
|
206
223
|
|
|
207
|
-
let empty_props = JsValue::Object(BTreeMap::new());
|
|
208
224
|
let ssr_defaults = entry_obj.get("ssrDefaults").cloned().unwrap_or_else(|| JsValue::Object(BTreeMap::new()));
|
|
209
|
-
|
|
225
|
+
|
|
226
|
+
// Apply any STATIC `signal_init` overrides by REPLACING the
|
|
227
|
+
// overridden key's entry with the bare override value --
|
|
228
|
+
// `resolve_child_vars`'s non-object-entry branch (`runtime.rs`) uses
|
|
229
|
+
// a bare value as-is, unconditionally (ignoring caller props), so an
|
|
230
|
+
// overridden key now WINS over a same-named caller prop. This is a
|
|
231
|
+
// DELIBERATE FLIP from the prior contract, not a preservation of it:
|
|
232
|
+
// previously this function flattened `ssr_defaults` against an EMPTY
|
|
233
|
+
// props document at registration time and `render_child` applied the
|
|
234
|
+
// caller's props LAST (`vars = defaults; vars.extend(props)`), so a
|
|
235
|
+
// caller prop always beat a `signal_init` override. Now
|
|
236
|
+
// `render_child` applies the RESOLVED extras last (`vars = props;
|
|
237
|
+
// vars.extend(extra)`), so an override -- and any `propName` that
|
|
238
|
+
// resolves against the real caller props -- wins instead. This
|
|
239
|
+
// matches the Python/PHP/Perl ports, where `signal_init_fn`'s
|
|
240
|
+
// returned `extra` (or the propName-resolved `_derive_stash_from_
|
|
241
|
+
// defaults` output) is merged LAST over `props`
|
|
242
|
+
// (`{**props, **extra}` / `props.merge(extra)`) rather than first.
|
|
243
|
+
// Resolution against the REAL caller props now happens PER-CALL,
|
|
244
|
+
// inside `render_child`, against these RAW (un-flattened) defaults
|
|
245
|
+
// -- registering with an EMPTY props document at THIS point (the
|
|
246
|
+
// pre-#2524-fix shape) is exactly the production bug: it flattened
|
|
247
|
+
// via `derive_stash_from_defaults` before any caller was known,
|
|
248
|
+
// discarding `propName` so an aliased destructured prop's
|
|
249
|
+
// CALLER-facing key could never later resolve.
|
|
250
|
+
let mut defaults_map = match &ssr_defaults {
|
|
251
|
+
JsValue::Object(m) => m.clone(),
|
|
252
|
+
_ => BTreeMap::new(),
|
|
253
|
+
};
|
|
210
254
|
if let Some(overrides) = signal_init.get(®istry_key) {
|
|
211
|
-
if let
|
|
255
|
+
if let Some(over) = overrides.as_object() {
|
|
212
256
|
for (k, v) in over {
|
|
213
|
-
|
|
257
|
+
defaults_map.insert(k.clone(), v.clone());
|
|
214
258
|
}
|
|
215
259
|
}
|
|
216
260
|
}
|
|
261
|
+
let defaults = JsValue::Object(defaults_map);
|
|
217
262
|
|
|
263
|
+
// `param_names` carries the CALLER-facing name (`propName`), not the
|
|
264
|
+
// entry's own (local) key -- `render_child`'s rest-bag "keep" set
|
|
265
|
+
// compares against child props keyed by whatever the calling
|
|
266
|
+
// template passed, not the child's local binding (#2524).
|
|
218
267
|
let (rest_props_name, param_names) = match ssr_defaults.as_object() {
|
|
219
268
|
Some(m) => {
|
|
220
269
|
let mut rest = None;
|
|
@@ -223,8 +272,8 @@ pub fn register_components_from_manifest(
|
|
|
223
272
|
let Some(dm) = d.as_object() else { continue };
|
|
224
273
|
if matches!(dm.get("isRestProps"), Some(JsValue::Bool(true))) {
|
|
225
274
|
rest = Some(name.clone());
|
|
226
|
-
} else if dm.
|
|
227
|
-
params.push(
|
|
275
|
+
} else if let Some(prop_name) = dm.get("propName").and_then(|v| v.as_str()) {
|
|
276
|
+
params.push(prop_name.to_string());
|
|
228
277
|
}
|
|
229
278
|
}
|
|
230
279
|
(rest, params)
|
|
@@ -237,7 +286,7 @@ pub fn register_components_from_manifest(
|
|
|
237
286
|
ChildRendererSpec {
|
|
238
287
|
component_name: template_name.clone(),
|
|
239
288
|
template: template_name,
|
|
240
|
-
ssr_defaults:
|
|
289
|
+
ssr_defaults: defaults,
|
|
241
290
|
rest_props_name,
|
|
242
291
|
param_names,
|
|
243
292
|
},
|
package/runtime/src/runtime.rs
CHANGED
|
@@ -796,13 +796,19 @@ pub struct ChildRendererSpec {
|
|
|
796
796
|
pub component_name: String,
|
|
797
797
|
/// snake_case `.j2` template base name.
|
|
798
798
|
pub template: String,
|
|
799
|
-
/// Static ssrDefaults
|
|
800
|
-
///
|
|
801
|
-
///
|
|
802
|
-
///
|
|
803
|
-
///
|
|
804
|
-
///
|
|
805
|
-
|
|
799
|
+
/// Static ssrDefaults, RAW -- the FULL `{value, propName?, isRestProps?}`
|
|
800
|
+
/// wrapper shape (or a bare value for a propName-less entry), exactly as
|
|
801
|
+
/// `extractSsrDefaults` emits it. Resolved PER-CALL against the real
|
|
802
|
+
/// caller props by [`crate::manifest::derive_stash_from_defaults`] inside
|
|
803
|
+
/// `render_child` below -- NOT flattened at registration time (that was
|
|
804
|
+
/// the #2524 production bug: flattening early, before the caller's props
|
|
805
|
+
/// are known, discards `propName` and so can never resolve an aliased
|
|
806
|
+
/// destructured prop's CALLER-facing key onto its local template var).
|
|
807
|
+
/// Kept as `JsValue` (not `MjValue`) because `derive_stash_from_defaults`
|
|
808
|
+
/// operates in the JSON-shaped `JsValue` domain; converted to `MjValue`
|
|
809
|
+
/// only for the resolved OUTPUT (see `render_child`'s docstring on why
|
|
810
|
+
/// child props otherwise stay `Value` end-to-end).
|
|
811
|
+
pub ssr_defaults: JsValue,
|
|
806
812
|
pub rest_props_name: Option<String>,
|
|
807
813
|
pub param_names: Vec<String>,
|
|
808
814
|
}
|
|
@@ -1115,11 +1121,32 @@ impl BfInstance {
|
|
|
1115
1121
|
data_key,
|
|
1116
1122
|
};
|
|
1117
1123
|
|
|
1118
|
-
// Seed template vars
|
|
1119
|
-
//
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1124
|
+
// Seed template vars through `resolve_child_vars`, resolving each
|
|
1125
|
+
// entry's `propName` against the REAL (already-mangled) caller props
|
|
1126
|
+
// -- mirrors the Ruby/Python/PHP/Perl runtime ports'
|
|
1127
|
+
// `derive_vars_from_defaults` / `_derive_stash_from_defaults` (#2524
|
|
1128
|
+
// SSR half: a flat `{**_defaults, **child_props}` merge never
|
|
1129
|
+
// resolves an aliased destructured prop's CALLER-facing key (`n`)
|
|
1130
|
+
// onto its local template var (`count`) -- `spec.ssr_defaults` is
|
|
1131
|
+
// the FULL `{value, propName?, isRestProps?}` shape, not flattened,
|
|
1132
|
+
// so this resolution has something to read). Caller props still win
|
|
1133
|
+
// for any key resolution did NOT touch (an extraneous, undeclared
|
|
1134
|
+
// prop); `extra` wins for the keys it DOES resolve -- the same
|
|
1135
|
+
// `props.merge(extra)` order every other language port uses.
|
|
1136
|
+
//
|
|
1137
|
+
// Deliberately NOT `crate::manifest::derive_stash_from_defaults`
|
|
1138
|
+
// (the `JsValue`-only free function): that would round-trip every
|
|
1139
|
+
// caller prop through `mj_to_js`/`js_to_mj`, which has no safe/
|
|
1140
|
+
// unsafe distinction and silently strips a SAFE `Value`'s flag (a
|
|
1141
|
+
// JSX children capture, `bf.string(...)`'d markup) -- exactly the
|
|
1142
|
+
// "children lost their safe flag" bug this method's docstring
|
|
1143
|
+
// above warns about. `resolve_child_vars` stays in the `MjValue`
|
|
1144
|
+
// domain for every caller-supplied value; only the STATIC fallback
|
|
1145
|
+
// (`d.value`, from the manifest/generated payload, never JSX
|
|
1146
|
+
// content) goes through `js_to_mj`.
|
|
1147
|
+
let extra = resolve_child_vars(&spec.ssr_defaults, &map);
|
|
1148
|
+
let mut vars: BTreeMap<String, MjValue> = map;
|
|
1149
|
+
vars.extend(extra);
|
|
1123
1150
|
|
|
1124
1151
|
let rendered = backend_minijinja::render_named_from_state_values(state, &spec.template, child.as_mj_value(), vars)?;
|
|
1125
1152
|
// chomp: remove at most one trailing newline.
|
|
@@ -1127,6 +1154,57 @@ impl BfInstance {
|
|
|
1127
1154
|
}
|
|
1128
1155
|
}
|
|
1129
1156
|
|
|
1157
|
+
/// `MjValue`-domain twin of [`crate::manifest::derive_stash_from_defaults`],
|
|
1158
|
+
/// used by [`BfInstance::render_child`] so a caller-supplied prop value --
|
|
1159
|
+
/// which may be a SAFE `Value` (a JSX children capture, `bf.string(...)`'d
|
|
1160
|
+
/// markup) -- is used AS-IS when `propName` resolves it, never round-tripped
|
|
1161
|
+
/// through the JSON-shaped `JsValue` domain (which has no safe/unsafe
|
|
1162
|
+
/// distinction and would silently strip that flag, double-escaping the
|
|
1163
|
+
/// child's HTML -- see `render_child`'s docstring). `ssr_defaults` is the
|
|
1164
|
+
/// RAW (un-flattened) `{value, propName?, isRestProps?}` shape; `props` is
|
|
1165
|
+
/// the caller's ALREADY-mangled prop map. Only the STATIC fallback
|
|
1166
|
+
/// (`d.value`, from the manifest/generated payload -- JSX prop values are
|
|
1167
|
+
/// never statically evaluable, so they're always `null` there) goes through
|
|
1168
|
+
/// [`js_to_mj`]. Returns keys already mangled (matching `props`'s own
|
|
1169
|
+
/// convention), ready to merge/overwrite directly.
|
|
1170
|
+
fn resolve_child_vars(ssr_defaults: &JsValue, props: &BTreeMap<String, MjValue>) -> BTreeMap<String, MjValue> {
|
|
1171
|
+
let mut extra = BTreeMap::new();
|
|
1172
|
+
let defaults_map = match ssr_defaults.as_object() {
|
|
1173
|
+
Some(m) => m,
|
|
1174
|
+
None => return extra,
|
|
1175
|
+
};
|
|
1176
|
+
for (name, d) in defaults_map {
|
|
1177
|
+
let key = mangle_ident(name);
|
|
1178
|
+
let dm = match d.as_object() {
|
|
1179
|
+
Some(m) => m,
|
|
1180
|
+
None => {
|
|
1181
|
+
// Bare (non-object) entry -- used as-is, mirrors every other
|
|
1182
|
+
// port's `ref($d) eq 'HASH'` / `isinstance(d, dict)` guard.
|
|
1183
|
+
extra.insert(key, js_to_mj(d));
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
let fallback = || js_to_mj(&dm.get("value").cloned().unwrap_or(JsValue::Null));
|
|
1188
|
+
if matches!(dm.get("isRestProps"), Some(JsValue::Bool(true))) {
|
|
1189
|
+
// The rest bag is already assembled (as a real `Value`, member
|
|
1190
|
+
// safe flags intact) under its own mangled key by the rest-bag
|
|
1191
|
+
// routing above -- prefer that live value over the static
|
|
1192
|
+
// fallback.
|
|
1193
|
+
let v = props.get(&key).cloned().unwrap_or_else(fallback);
|
|
1194
|
+
extra.insert(key, v);
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
let prop_name = dm.get("propName").and_then(|v| v.as_str());
|
|
1198
|
+
let from_props = prop_name.and_then(|pn| props.get(&mangle_ident(pn)));
|
|
1199
|
+
let v = match from_props {
|
|
1200
|
+
Some(pv) if !matches!(pv.kind(), ValueKind::None | ValueKind::Undefined) => pv.clone(),
|
|
1201
|
+
_ => fallback(),
|
|
1202
|
+
};
|
|
1203
|
+
extra.insert(key, v);
|
|
1204
|
+
}
|
|
1205
|
+
extra
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1130
1208
|
// ---------------------------------------------------------------------------
|
|
1131
1209
|
// minijinja::Value <-> JsValue conversion. minijinja 2.x represents EVERY
|
|
1132
1210
|
// sequence/mapping as a `Object` (`ObjectRepr::Seq`/`Map`/`Iterable`) --
|
|
@@ -146,6 +146,41 @@ fn register_from_flat_manifest_applies_signal_init_override() {
|
|
|
146
146
|
assert_eq!(html.trim(), "9");
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/// A `signal_init` override wins over a CALLER-SUPPLIED prop of the same
|
|
150
|
+
/// name, not just the manifest's static default (#2524 precedence flip --
|
|
151
|
+
/// see `manifest.rs`'s registration-site comment). Pre-#2524 this function
|
|
152
|
+
/// flattened `ssr_defaults` at registration time and `render_child` applied
|
|
153
|
+
/// the caller's props LAST, so a caller-supplied `level` would have beaten
|
|
154
|
+
/// the override; now `render_child` applies the resolved `signal_init`
|
|
155
|
+
/// override LAST, matching the Python/PHP/Perl ports' `{**props, **extra}`
|
|
156
|
+
/// / `props.merge(extra)` merge order.
|
|
157
|
+
#[test]
|
|
158
|
+
fn register_from_flat_manifest_override_wins_over_caller_prop() {
|
|
159
|
+
let dir = TempDir::new("override-vs-caller");
|
|
160
|
+
dir.write("root.j2", "{{ bf.render_child('widget', {'level': 3}) | safe }}");
|
|
161
|
+
dir.write("Widget.j2", "{{ bf.string(level) }}");
|
|
162
|
+
|
|
163
|
+
let manifest = obj(vec![(
|
|
164
|
+
"Widget",
|
|
165
|
+
obj(vec![
|
|
166
|
+
("markedTemplate", JsValue::String("templates/Widget.j2".into())),
|
|
167
|
+
("ssrDefaults", obj(vec![("level", obj(vec![("value", JsValue::Number(1.0))]))])),
|
|
168
|
+
]),
|
|
169
|
+
)]);
|
|
170
|
+
|
|
171
|
+
let mut signal_init = HashMap::new();
|
|
172
|
+
signal_init.insert("widget".to_string(), obj(vec![("level", JsValue::Number(9.0))]));
|
|
173
|
+
|
|
174
|
+
let session = RenderSession::new();
|
|
175
|
+
register_components_from_manifest(&session, &manifest, &signal_init);
|
|
176
|
+
|
|
177
|
+
let env = backend_minijinja::build_environment(&dir.0);
|
|
178
|
+
let root = BfInstance::root(Arc::clone(&session), "test".to_string());
|
|
179
|
+
let html = backend_minijinja::render_named(&env, "root", root.as_mj_value(), &JsValue::Object(BTreeMap::new())).unwrap();
|
|
180
|
+
// The caller passed `level: 3`, but the `signal_init` override (9) wins.
|
|
181
|
+
assert_eq!(html.trim(), "9");
|
|
182
|
+
}
|
|
183
|
+
|
|
149
184
|
/// `ui/<name>/index`-shaped entries (the component-library registry
|
|
150
185
|
/// convention Python's port targets) still register correctly.
|
|
151
186
|
#[test]
|
|
@@ -14,9 +14,8 @@
|
|
|
14
14
|
//! additionally exercises the actual `Object::call_method` dispatch path.
|
|
15
15
|
|
|
16
16
|
use barefootjs::num::JsValue;
|
|
17
|
-
use barefootjs::runtime::
|
|
17
|
+
use barefootjs::runtime::ChildRendererSpec;
|
|
18
18
|
use barefootjs::{backend_minijinja, BfInstance, RenderSession};
|
|
19
|
-
use minijinja::value::Value as MjValue;
|
|
20
19
|
use std::collections::BTreeMap;
|
|
21
20
|
use std::path::PathBuf;
|
|
22
21
|
use std::sync::Arc;
|
|
@@ -46,10 +45,11 @@ fn no_defaults() -> JsValue {
|
|
|
46
45
|
JsValue::Object(BTreeMap::new())
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
/// `ChildRendererSpec::ssr_defaults`, which is `
|
|
50
|
-
///
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
/// `ChildRendererSpec::ssr_defaults`, which is a RAW `JsValue` (the FULL
|
|
49
|
+
/// `{value, propName?, isRestProps?}` shape, resolved per-call against the
|
|
50
|
+
/// real caller props inside `render_child` -- see its docstring).
|
|
51
|
+
fn no_ssr_defaults() -> JsValue {
|
|
52
|
+
JsValue::Object(BTreeMap::new())
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
#[test]
|
|
@@ -128,7 +128,7 @@ fn rest_bag_routing_and_ssr_defaults_merge() {
|
|
|
128
128
|
ChildRendererSpec {
|
|
129
129
|
component_name: "Card".to_string(),
|
|
130
130
|
template: "card".to_string(),
|
|
131
|
-
ssr_defaults:
|
|
131
|
+
ssr_defaults: JsValue::Object(defaults),
|
|
132
132
|
rest_props_name: Some("rest".to_string()),
|
|
133
133
|
param_names: vec!["title".to_string()],
|
|
134
134
|
},
|
|
@@ -138,6 +138,40 @@ fn rest_bag_routing_and_ssr_defaults_merge() {
|
|
|
138
138
|
assert_eq!(out, "Hi|Sub|E|dark");
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
#[test]
|
|
142
|
+
fn aliased_prop_resolves_callers_facing_key_onto_local_template_var() {
|
|
143
|
+
// #2524 (SSR half): a renaming destructure (`{ n: count }`) keys the
|
|
144
|
+
// ssrDefaults entry by the LOCAL binding (`count`) but its `propName`
|
|
145
|
+
// is the CALLER-facing key (`n`) -- the call site passes `n` (it has no
|
|
146
|
+
// visibility into how the child renames it internally), so
|
|
147
|
+
// `render_child` must resolve `n` onto the child template's `count`,
|
|
148
|
+
// not leave `count` at its static (`null`) fallback.
|
|
149
|
+
let dir = TempDir::new("aliased");
|
|
150
|
+
dir.write("root.j2", "{{ bf.render_child('badge', {'n': 7}) }}");
|
|
151
|
+
dir.write("badge.j2", "{{ count }}");
|
|
152
|
+
|
|
153
|
+
let env = backend_minijinja::build_environment(&dir.0);
|
|
154
|
+
let session = RenderSession::new();
|
|
155
|
+
let mut count_entry = BTreeMap::new();
|
|
156
|
+
count_entry.insert("value".to_string(), JsValue::Null);
|
|
157
|
+
count_entry.insert("propName".to_string(), JsValue::from("n"));
|
|
158
|
+
let mut defaults = BTreeMap::new();
|
|
159
|
+
defaults.insert("count".to_string(), JsValue::Object(count_entry));
|
|
160
|
+
session.register_child_renderer(
|
|
161
|
+
"badge".to_string(),
|
|
162
|
+
ChildRendererSpec {
|
|
163
|
+
component_name: "Badge".to_string(),
|
|
164
|
+
template: "badge".to_string(),
|
|
165
|
+
ssr_defaults: JsValue::Object(defaults),
|
|
166
|
+
rest_props_name: None,
|
|
167
|
+
param_names: vec!["n".to_string()],
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
let root = BfInstance::root(session, "Root_test");
|
|
171
|
+
let out = backend_minijinja::render_entry(&env, "root", root.as_mj_value(), &no_defaults(), &[]).unwrap();
|
|
172
|
+
assert_eq!(out, "7");
|
|
173
|
+
}
|
|
174
|
+
|
|
141
175
|
#[test]
|
|
142
176
|
fn loop_child_without_slot_gets_a_fresh_component_prefixed_scope() {
|
|
143
177
|
let dir = TempDir::new("loopchild");
|
|
@@ -22,20 +22,4 @@ export const renderDivergences: RenderDivergences = {
|
|
|
22
22
|
// instead of a fixed regex-shape catalogue) now correctly seeds `todos`
|
|
23
23
|
// from `(props.initialTodos ?? []).map(t => ({ ...t, editing: false }))`.
|
|
24
24
|
|
|
25
|
-
// Onboarding TSX-fidelity fixtures (PR #2461): `expectedHtml` was
|
|
26
|
-
// hand-authored to the CORRECT output while the emission bug lived in
|
|
27
|
-
// the shared compiler layer — every adapter, including the Hono
|
|
28
|
-
// reference, used to emit the broken form. That shared-layer defect
|
|
29
|
-
// (#2460) is now FIXED (b4f5075): `expectedHtml` is generated from the
|
|
30
|
-
// Hono reference like any other fixture. The remaining gap is
|
|
31
|
-
// per-template-adapter — this adapter still keys its template vars /
|
|
32
|
-
// ssr-defaults / props bridge off the local binding instead of
|
|
33
|
-
// `sourceName ?? name` — tracked by #2524 (the 7 silent template
|
|
34
|
-
// adapters). Graduate by applying the same `sourceName ?? name` fix to
|
|
35
|
-
// this adapter's emission path and deleting these lines (and the
|
|
36
|
-
// matching hono `skipJsx` entries, already gone).
|
|
37
|
-
'aliased-destructured-prop':
|
|
38
|
-
'aliased destructured prop `{ n: count }` loses its rename — template vars, ssr-defaults, and the props bridge all key off the local name, so the prop is always undefined (https://github.com/piconic-ai/barefootjs/issues/2524)',
|
|
39
|
-
'composite-row-child-aliased-prop':
|
|
40
|
-
'same defect as `aliased-destructured-prop` (#2524), inside a keyed `.map()` loop row: the nested child\'s renamed prop (`{ n: count }`) is always undefined, so both rows render an empty count (https://github.com/piconic-ai/barefootjs/issues/2524)',
|
|
41
25
|
}
|
package/src/test-render.ts
CHANGED
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
* that closes that gap.
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
-
import { compileJSX, extractSsrDefaults, importsSearchParams, evaluateSignalInit } from '@barefootjs/jsx'
|
|
32
|
-
import type { ComponentIR } from '@barefootjs/jsx'
|
|
31
|
+
import { compileJSX, extractSsrDefaults, deriveStashFromDefaults, importsSearchParams, evaluateSignalInit } from '@barefootjs/jsx'
|
|
32
|
+
import type { ComponentIR, SsrDefault } from '@barefootjs/jsx'
|
|
33
33
|
import { mkdir, rm } from 'node:fs/promises'
|
|
34
34
|
import { resolve } from 'node:path'
|
|
35
35
|
|
|
@@ -362,24 +362,30 @@ function collectImportedComponentNames(ir: ComponentIR): string[] {
|
|
|
362
362
|
* design doc; see `runtime/src/backend_minijinja.rs`'s `render_child` for
|
|
363
363
|
* the Rust-side closure — the near-verbatim structural counterpart of
|
|
364
364
|
* adapter-jinja's `buildChildRenderers`, which instead emitted Python
|
|
365
|
-
* closure SOURCE per child). `ssr_defaults`
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
365
|
+
* closure SOURCE per child). `ssr_defaults` is sent VERBATIM — `value` /
|
|
366
|
+
* `propName` / `isRestProps` intact — so the Rust runtime's
|
|
367
|
+
* `derive_stash_from_defaults` (called per-call, inside `render_child`,
|
|
368
|
+
* against the REAL child props) can resolve an aliased destructured prop's
|
|
369
|
+
* CALLER-facing key (`n`) onto its local template var (`count`). Sending
|
|
370
|
+
* only the flattened static `value` here was the #2524 SSR-half bug — it
|
|
371
|
+
* left nothing for that resolution to read. `param_names` carries the
|
|
372
|
+
* CALLER-facing name (`sourceName ?? name`) per prop — `render_child`'s
|
|
373
|
+
* rest-bag "keep" set compares against child props keyed by whatever the
|
|
374
|
+
* calling template passed, not the child's local binding.
|
|
369
375
|
*/
|
|
370
376
|
function buildChildrenPayload(
|
|
371
377
|
childTemplates: Map<string, { template: string; ir: ComponentIR }>,
|
|
372
378
|
): Array<{
|
|
373
379
|
name: string
|
|
374
380
|
template: string
|
|
375
|
-
ssr_defaults: Record<string,
|
|
381
|
+
ssr_defaults: Record<string, SsrDefault>
|
|
376
382
|
rest_props_name: string | null
|
|
377
383
|
param_names: string[]
|
|
378
384
|
}> {
|
|
379
385
|
const out: Array<{
|
|
380
386
|
name: string
|
|
381
387
|
template: string
|
|
382
|
-
ssr_defaults: Record<string,
|
|
388
|
+
ssr_defaults: Record<string, SsrDefault>
|
|
383
389
|
rest_props_name: string | null
|
|
384
390
|
param_names: string[]
|
|
385
391
|
}> = []
|
|
@@ -388,29 +394,14 @@ function buildChildrenPayload(
|
|
|
388
394
|
out.push({
|
|
389
395
|
name: componentName,
|
|
390
396
|
template: toSnakeCase(componentName),
|
|
391
|
-
ssr_defaults:
|
|
397
|
+
ssr_defaults: ssrDefaults,
|
|
392
398
|
rest_props_name: childIR.metadata.restPropsName ?? null,
|
|
393
|
-
param_names: (childIR.metadata.propsParams ?? []).map(p => p.name),
|
|
399
|
+
param_names: (childIR.metadata.propsParams ?? []).map(p => p.sourceName ?? p.name),
|
|
394
400
|
})
|
|
395
401
|
}
|
|
396
402
|
return out
|
|
397
403
|
}
|
|
398
404
|
|
|
399
|
-
/** Reduce an ssrDefaults map to its static fallback values (plain JS object). */
|
|
400
|
-
function ssrDefaultsToVars(defaults: Record<string, unknown>): Record<string, unknown> {
|
|
401
|
-
const out: Record<string, unknown> = {}
|
|
402
|
-
for (const [name, d] of Object.entries(defaults)) {
|
|
403
|
-
// ssrDefaults entries are `{ value, propName?, isRestProps? }` or a
|
|
404
|
-
// bare value. The child renderer's caller props win, so we only need
|
|
405
|
-
// the static fallback `value` here.
|
|
406
|
-
out[name] =
|
|
407
|
-
d && typeof d === 'object' && 'value' in (d as Record<string, unknown>)
|
|
408
|
-
? (d as Record<string, unknown>).value
|
|
409
|
-
: d
|
|
410
|
-
}
|
|
411
|
-
return out
|
|
412
|
-
}
|
|
413
|
-
|
|
414
405
|
/**
|
|
415
406
|
* Convert PascalCase to snake_case for template naming (matches the
|
|
416
407
|
* adapter's `toTemplateName`).
|
|
@@ -445,24 +436,31 @@ function buildVars(
|
|
|
445
436
|
const vars: Record<string, unknown> = {}
|
|
446
437
|
|
|
447
438
|
// Prop params with defaults (before signals, so signals can reference them).
|
|
439
|
+
// Seeded through the shared `deriveStashFromDefaults` (the TS twin of the
|
|
440
|
+
// production `derive_stash_from_defaults` the Rust runtime calls at
|
|
441
|
+
// render time for child components — see `render_child` in
|
|
442
|
+
// `runtime/src/runtime.rs`) so an aliased destructured prop's
|
|
443
|
+
// CALLER-facing key (`propName`, e.g. `n` for `{ n: count }`) is
|
|
444
|
+
// honoured, not just the local template var name (#2524 SSR half).
|
|
445
|
+
// `props` is keyed by the caller-facing name, exactly what `propName`
|
|
446
|
+
// resolves against.
|
|
447
|
+
const rootSsrDefaults = extractSsrDefaults(ir.metadata) ?? {}
|
|
448
|
+
const derivedProps = deriveStashFromDefaults(rootSsrDefaults, props ?? {})
|
|
448
449
|
for (const param of ir.metadata.propsParams) {
|
|
449
|
-
if (
|
|
450
|
-
if (param.defaultValue) {
|
|
451
|
-
const value = evaluateSignalInit(param.defaultValue.trim(), props)
|
|
452
|
-
if (value !== null) {
|
|
453
|
-
vars[param.name] = value
|
|
454
|
-
continue
|
|
455
|
-
}
|
|
456
|
-
}
|
|
450
|
+
if (param.isRest) continue
|
|
457
451
|
// No default + no caller value: pass `null` (Rust's `None`/minijinja
|
|
458
452
|
// Undefined) so a bare reference to an optional prop doesn't fault
|
|
459
453
|
// before its falsy branch elides.
|
|
460
|
-
vars[param.name] = null
|
|
454
|
+
vars[param.name] = derivedProps[param.name] ?? null
|
|
461
455
|
}
|
|
462
456
|
|
|
463
457
|
// Route undeclared props into the rest bag (`bf.spread_attrs($<rest>)`).
|
|
464
458
|
const restPropsName = ir.metadata.restPropsName
|
|
465
|
-
|
|
459
|
+
// Caller-facing keys — an aliased destructured prop's DECLARED set for
|
|
460
|
+
// rest-bag routing must match what the caller actually sent (`n`), not
|
|
461
|
+
// the local binding (`count`), or the caller's own prop silently gets
|
|
462
|
+
// swept into the rest bag as an undeclared extra (#2524).
|
|
463
|
+
const declaredParams = new Set(ir.metadata.propsParams.map(p => p.sourceName ?? p.name))
|
|
466
464
|
const restBagEntries: Array<[string, unknown]> = []
|
|
467
465
|
if (restPropsName && props) {
|
|
468
466
|
for (const [key, value] of Object.entries(props)) {
|
|
@@ -477,11 +475,20 @@ function buildVars(
|
|
|
477
475
|
vars[restPropsName] = Object.fromEntries(restBagEntries)
|
|
478
476
|
}
|
|
479
477
|
|
|
480
|
-
// User props.
|
|
478
|
+
// User props. Skip a key that's already a declared param's LOCAL template
|
|
479
|
+
// var name — `derivedProps` above already resolved that var correctly
|
|
480
|
+
// (through `propName`, for an aliased prop); re-assigning the raw
|
|
481
|
+
// `props[key]` here would silently clobber it with an UNRELATED value
|
|
482
|
+
// whenever a caller happens to also pass a same-spelled-as-local-name prop
|
|
483
|
+
// that isn't this param's actual `propName` (#2524 — surfaced by the
|
|
484
|
+
// aliased-destructured-prop generated data points, which pass both `n`
|
|
485
|
+
// (the real propName) and an incidental `count` key).
|
|
486
|
+
const localParamNames = new Set(ir.metadata.propsParams.map(p => p.name))
|
|
481
487
|
if (props) {
|
|
482
488
|
for (const [key, value] of Object.entries(props)) {
|
|
483
489
|
if (key.startsWith('__')) continue
|
|
484
490
|
if (routedKeys.has(key)) continue
|
|
491
|
+
if (localParamNames.has(key)) continue
|
|
485
492
|
if (
|
|
486
493
|
typeof value === 'string' ||
|
|
487
494
|
typeof value === 'number' ||
|
|
@@ -507,11 +514,9 @@ function buildVars(
|
|
|
507
514
|
|
|
508
515
|
// Memo values seeded from the statically-evaluated ssrDefaults, same
|
|
509
516
|
// as the production plugin's before_render hook.
|
|
510
|
-
const ssrDefaults = extractSsrDefaults(ir.metadata) ?? {}
|
|
511
517
|
for (const memo of ir.metadata.memos) {
|
|
512
|
-
const entry =
|
|
513
|
-
|
|
514
|
-
vars[memo.name] = value ?? 0
|
|
518
|
+
const entry = rootSsrDefaults[memo.name]
|
|
519
|
+
vars[memo.name] = entry ? (entry.value ?? 0) : 0
|
|
515
520
|
}
|
|
516
521
|
|
|
517
522
|
return vars
|