@lmjs/core 2.0.2 → 2.1.1

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/src/_re.js CHANGED
@@ -190,7 +190,17 @@ function _x(_x) {
190
190
  // so a stale tail from a prior _vt.Global read (which never hit
191
191
  // "View" to reset it) would otherwise get prepended to the next
192
192
  // chain and corrupt which key set()'s update(key) fires for.
193
- if (key == "View" || key == "Global") currPath = [];
193
+ // 2026-09-18: generalized from a hardcoded `key == "View" ||
194
+ // key == "Global"` name list (which needed editing every time a
195
+ // new permanent root, e.g. "Widgets", got added) to the one
196
+ // thing that's actually invariant: `target === _x` is only ever
197
+ // true for THIS proxy's own outermost target (the raw root
198
+ // object this whole closure was built from) — any nested proxy
199
+ // in the tree wraps some OTHER object as its target, never `_x`
200
+ // itself. So this fires on any fresh top-level access
201
+ // (`_vt.View`, `_vt.Global`, `_vt.Widgets`, ...) regardless of
202
+ // the key name, with nothing new to maintain per root added.
203
+ if (target === _x) currPath = [];
194
204
  currPath.push(key);
195
205
  if (typeof target[key] === 'object' && target[key] !== null && key != "_re") {
196
206
  // cl("salsaa", target, key)
@@ -287,11 +297,55 @@ function _x(_x) {
287
297
  // an uncaught exception there aborts every remaining statement in the
288
298
  // whole bundle file, breaking the entire app on every real production
289
299
  // site with this pattern. Found deploying lumenjs.com's own real build.
300
+ // "Widgets" (2026-09-18): a third permanent root, sibling of Global/View,
301
+ // generalizing V1's hardcoded hasNav/hasHeader/hasFooter widget-loading past
302
+ // its fixed 3-name list — see renderView()'s layout branch below for the
303
+ // resolve/mount/diff logic, and fcs.js's annotateLayoutRegions() for how a
304
+ // layout's own bare attributes (e.g. `<nav nav>`) become the region names
305
+ // used to key this object. Deliberately NOT an ancestor of View in the
306
+ // scope tree (widget regions and [body] are DOM SIBLINGS under the layout
307
+ // root, never one containing the other — unlike a subview's real, singular
308
+ // parent, there's no single unambiguous "the" widget a view could shadow
309
+ // into) — each entry gets its own real `._re` instead (see
310
+ // _dispatchVarsUpdate's generic owner-walk below, which already handles
311
+ // this with zero special-casing once an entry has `._re` set, same as any
312
+ // subview). Starts empty; populated lazily, one entry per declared region
313
+ // name, the first time a layout using that region is mounted.
290
314
  let _vt = _x({
291
315
  "View": new _v({}),
292
- "Global": { "vars": {}, "fns": {} }
316
+ "Global": { "vars": {}, "fns": {} },
317
+ "Widgets": {}
293
318
  });
294
319
 
320
+ // 2026-09-18: shared by getVal()/evalExp()/concatVarsAtLevel() below, so the
321
+ // "first-declared widget wins on a name collision" rule is defined exactly
322
+ // once. Object key iteration order for string keys is real insertion order,
323
+ // so this naturally checks widgets in the order they were first mounted —
324
+ // which, in practice, is document order (renderView()'s layout branch walks
325
+ // `hstL.regions`, itself built in document order by fcs.js's
326
+ // annotateLayoutRegions()).
327
+ function _lookupInWidgets(name) {
328
+ for (const wname in _vt.Widgets) {
329
+ if (_vt.Widgets[wname].vars.hasOwnProperty(name)) return _vt.Widgets[wname].vars[name];
330
+ }
331
+ return undefined;
332
+ }
333
+
334
+ // concatVarsAtLevel() below merges via object spread, not the loop-with-
335
+ // break shape _lookupInWidgets() uses — spread's "later wins" needs the
336
+ // widgets merged in REVERSE declaration order so the first-declared widget
337
+ // still wins a name collision, matching _lookupInWidgets()'s own semantics
338
+ // exactly (both must agree, since getVal()/evalExp() use one and
339
+ // concatVarsAtLevel()/lookup()/scopedEval() use the other for the same read).
340
+ function _mergedWidgetsVars() {
341
+ let out = {};
342
+ let names = Object.keys(_vt.Widgets).reverse();
343
+ for (const wname of names) {
344
+ out = { ...out, ..._vt.Widgets[wname].vars };
345
+ }
346
+ return out;
347
+ }
348
+
295
349
  class _lm {
296
350
  _RealDOM = [];
297
351
  _effects = {};
@@ -1073,10 +1127,17 @@ class _lm {
1073
1127
  // lookup()/scopedEval(); getVal() (what mustache text nodes
1074
1128
  // actually call) had its own separate, unmerged read here
1075
1129
  // and was silently rendering these as empty.
1130
+ // 2026-09-18: inserted a middle tier — _lookupInWidgets()
1131
+ // (all currently-mounted widgets' vars, first-declared
1132
+ // wins) — between View and Global, per this session's
1133
+ // agreed read-fallback order.
1076
1134
  let __name = this.reactiveVariables[i];
1077
- vars[__name] = _vt.View.vars.hasOwnProperty(__name)
1078
- ? _vt.View.vars[__name]
1079
- : _vt.Global.vars[__name];
1135
+ if (_vt.View.vars.hasOwnProperty(__name)) {
1136
+ vars[__name] = _vt.View.vars[__name];
1137
+ } else {
1138
+ let _wv = _lookupInWidgets(__name);
1139
+ vars[__name] = _wv !== undefined ? _wv : _vt.Global.vars[__name];
1140
+ }
1080
1141
  }
1081
1142
  for (const ky in this.vrs) {
1082
1143
  if (Object.prototype.hasOwnProperty.call(this.vrs, ky)) {
@@ -1207,8 +1268,10 @@ class _lm {
1207
1268
  // Global vars (index.js's top-level state, see _vt.Global
1208
1269
  // above) sit at the lowest priority here — a view or a
1209
1270
  // narrower scope declaring the same name shadows it, same
1210
- // as real JS scoping would.
1211
- concatenatedVars = { ..._vt.Global.vars, ...parent.view.vars, ...concatenatedVars };
1271
+ // as real JS scoping would. 2026-09-18: widgets' vars sit
1272
+ // one tier above Global, below the view's own — see
1273
+ // _mergedWidgetsVars()'s comment for the ordering rationale.
1274
+ concatenatedVars = { ..._vt.Global.vars, ..._mergedWidgetsVars(), ...parent.view.vars, ...concatenatedVars };
1212
1275
  }
1213
1276
  return concatenatedVars;
1214
1277
  }
@@ -1462,12 +1525,11 @@ class _lm {
1462
1525
  // minified code) — [sl]/[color]/[time]/[date]/[datetime] elements
1463
1526
  // become their respective widgets on mount, no explicit script
1464
1527
  // call needed, matching V1's documented behavior (lumenjs-
1465
- // spec.md §5.6/§6.7). Built into core directly rather than living
1466
- // only in the --with-plugins bundle (explicit choice, 2026-09-16)
1467
- // — autoInitPlugins() itself guards every call on the matching
1468
- // $.fn.* method actually existing, so a plain (no --with-plugins)
1469
- // project's dom-shim-based bundle silently no-ops instead of
1470
- // throwing when it encounters one of these attributes.
1528
+ // spec.md §5.6/§6.7). Built into core directly, always available
1529
+ // (explicit choice, 2026-09-16) — autoInitPlugins() itself guards
1530
+ // every call on the matching $.fn.* method actually existing, so a
1531
+ // plain project with no plugins selected silently no-ops instead
1532
+ // of throwing when it encounters one of these attributes.
1471
1533
  autoInitPlugins(_el, attrs);
1472
1534
 
1473
1535
  return _el;
@@ -1520,10 +1582,15 @@ class _lm {
1520
1582
  // value scopedEval() would otherwise have found in
1521
1583
  // _vt.Global.vars — breaking :if/:for conditions on any
1522
1584
  // index.js-declared var, not just mustache text (getVal()).
1585
+ // 2026-09-18: same middle tier as getVal() — see its
1586
+ // matching comment.
1523
1587
  let __name = vars[i];
1524
- rvars[__name] = _vt.View.vars.hasOwnProperty(__name)
1525
- ? _vt.View.vars[__name]
1526
- : _vt.Global.vars[__name];
1588
+ if (_vt.View.vars.hasOwnProperty(__name)) {
1589
+ rvars[__name] = _vt.View.vars[__name];
1590
+ } else {
1591
+ let _wv = _lookupInWidgets(__name);
1592
+ rvars[__name] = _wv !== undefined ? _wv : _vt.Global.vars[__name];
1593
+ }
1527
1594
  }
1528
1595
  // 2026-09-17, real bug found and fixed: getVal() (mustache
1529
1596
  // text, e.g. {{f.name}}) has always merged this.vrs in after
@@ -2284,12 +2351,11 @@ async function fireRenderHook(cx, n, containerEl, extra) {
2284
2351
  // (SBEACDN/s.beacdn.com/beajs/core.js:2846/3834, confirmed against the
2285
2352
  // real V1 build root this session already located, not reverse-engineered
2286
2353
  // from minified code). Called from createEl() on every element's mount.
2287
- // Deliberately built into packages/core's own engine rather than kept
2288
- // inside the --with-plugins bundle only — every call below is guarded on
2289
- // the matching $.fn.* method actually existing, so a plain project (no
2290
- // --with-plugins, dom-shim-based, none of these libraries loaded) safely
2291
- // no-ops instead of throwing when a view happens to use one of these
2292
- // attributes.
2354
+ // Deliberately built into packages/core's own engine, always available
2355
+ // every call below is guarded on the matching $.fn.* method actually
2356
+ // existing, so a plain project (dom-shim-based, none of these libraries
2357
+ // loaded) safely no-ops instead of throwing when a view happens to use one
2358
+ // of these attributes.
2293
2359
  function autoInitPlugins(el, attrs) {
2294
2360
  if (!attrs) return;
2295
2361
  try {
@@ -2400,9 +2466,9 @@ var _dbcrsTime = 250;
2400
2466
 
2401
2467
  // Real V1 source, ported as-is (SBEACDN core.js:3834-3891) — bootstrap-
2402
2468
  // datetimepicker option builder + optional start/end date-linking. Not
2403
- // documented in lumenjs-spec.md (see PROVENANCE.md's --with-plugins
2404
- // section); this exact option shape was confirmed directly against V1's
2405
- // real source once it was located this session, not reverse-engineered.
2469
+ // documented in lumenjs-spec.md (see PROVENANCE.md's plugins section);
2470
+ // this exact option shape was confirmed directly against V1's real source
2471
+ // once it was located this session, not reverse-engineered.
2406
2472
  function dtp(el, t) {
2407
2473
  el.removeAttr(t);
2408
2474
  let opts = {
@@ -2544,7 +2610,6 @@ async function renderView(n, isSub, d, type = 'views', viewsArr, scopeBase = ["V
2544
2610
  let filePath = "src/views/" + n + ".view"
2545
2611
  if (type == 'layouts') filePath = "src/layouts/" + n + ".layout";
2546
2612
 
2547
- cl(arguments);
2548
2613
  let fileKey = btoa(filePath);
2549
2614
  if (!isSub) {
2550
2615
  View.props = d ?? {};
@@ -2657,12 +2722,21 @@ async function renderView(n, isSub, d, type = 'views', viewsArr, scopeBase = ["V
2657
2722
 
2658
2723
  let appSelector = appSettings.App ?? "[app]";
2659
2724
  var appContainer = $(appSelector);
2725
+ // 2026-09-18: whether THIS call just replaced the layout's
2726
+ // own DOM wholesale — needed below by the widget
2727
+ // resolve/mount/diff pass, since a widget whose resolution
2728
+ // hasn't changed still needs its already-rendered nodes
2729
+ // reattached to the fresh layout element (the old one they
2730
+ // were living in is gone), even though nothing about the
2731
+ // widget itself needs re-rendering.
2732
+ let layoutJustSwapped = false;
2660
2733
  if (appContainer.length) {
2661
2734
  let currentLayout = $(appSelector).data('layout');
2662
2735
  if (currentLayout != layout) {
2663
2736
  let _rel = await renderHST(hstL, layout, 'layout');
2664
2737
  appContainer.data('layout', layout).html(_rel._RealDOM);
2665
2738
  _rel.renderAll();
2739
+ layoutJustSwapped = true;
2666
2740
  } else {
2667
2741
  // cl("Same Layout");
2668
2742
  }
@@ -2672,6 +2746,59 @@ async function renderView(n, isSub, d, type = 'views', viewsArr, scopeBase = ["V
2672
2746
  appContainer = $(appSelector);
2673
2747
  appContainer.data('layout', layout).html(_rel._RealDOM);
2674
2748
  _rel.renderAll();
2749
+ layoutJustSwapped = true;
2750
+ }
2751
+
2752
+ // Persistent layout widgets (_vt.Widgets, 2026-09-18) — see
2753
+ // that object's own declaration comment for the design.
2754
+ // Runs on EVERY navigation, not gated on layoutJustSwapped:
2755
+ // a view's own settings (hasNav: false, hasNav: "nav2", ...)
2756
+ // can change what belongs in a region even when the layout
2757
+ // itself didn't change at all. hstL.regions is precomputed
2758
+ // build-time by fcs.js's annotateLayoutRegions() — the bare
2759
+ // (valueless) attribute names the CURRENTLY MOUNTED layout
2760
+ // itself declares (e.g. ["header","nav","footer"] for
2761
+ // `<header header>`/`<nav nav>`/`<footer footer>`).
2762
+ let declaredRegions = (hstL && hstL.regions) || [];
2763
+ for (const regionName of declaredRegions) {
2764
+ let settingsKey = "has" + regionName[0].toUpperCase() + regionName.slice(1);
2765
+ let want = _re.view.settings.hasOwnProperty(settingsKey) ? _re.view.settings[settingsKey] : true;
2766
+ let resolvedFile = want === false ? null : (want === true ? regionName : want);
2767
+
2768
+ let w = _vt.Widgets[regionName] || (_vt.Widgets[regionName] = { vars: {}, fns: {}, views: [], _re: null, _resolvedFile: undefined });
2769
+
2770
+ if (resolvedFile !== w._resolvedFile) {
2771
+ // Resolution genuinely changed since the last time
2772
+ // this region was resolved — mount fresh, or clear.
2773
+ if (resolvedFile === null) {
2774
+ $('[' + regionName + ']').html('');
2775
+ w._re = null;
2776
+ } else {
2777
+ let wFilePath = "src/views/widgets/" + resolvedFile + ".view";
2778
+ let wHst = _payload['views'][btoa(wFilePath)];
2779
+ if (wHst) {
2780
+ let _rew = await renderHST(wHst, resolvedFile, 'widget', undefined, null, ["Widgets", regionName], w.vars, w.fns, w.views);
2781
+ w._re = _rew;
2782
+ $('[' + regionName + ']').html(_rew._RealDOM);
2783
+ _rew.renderAll();
2784
+ }
2785
+ }
2786
+ w._resolvedFile = resolvedFile;
2787
+ } else if (layoutJustSwapped && w._re) {
2788
+ // Same widget as before, but the layout element it
2789
+ // lives in was just replaced wholesale — reattach
2790
+ // the EXISTING rendered nodes (.html() moves real
2791
+ // node references, same as [body]/[slot] below,
2792
+ // it doesn't clone) rather than re-rendering. The
2793
+ // widget's own state (w.vars/.fns/.views) never
2794
+ // lived on the old DOM to begin with, so nothing
2795
+ // was lost — only the attachment point needed
2796
+ // fixing up.
2797
+ $('[' + regionName + ']').html(w._re._RealDOM);
2798
+ }
2799
+ // else: nothing changed and the layout didn't just
2800
+ // swap — this region's DOM/state is left completely
2801
+ // untouched, on purpose (the whole point of this).
2675
2802
  }
2676
2803
 
2677
2804
  // return;