@lmjs/core 2.0.0 → 2.0.2

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
@@ -17,6 +17,7 @@ class _v {
17
17
  static name;
18
18
  static type;
19
19
  static vars;
20
+ static fns;
20
21
  static rvs;
21
22
  static _pv;
22
23
  static mx;
@@ -29,6 +30,16 @@ class _v {
29
30
  this.hst = obj.hst ?? [];
30
31
  this.views = obj.views ?? [];
31
32
  this.vars = obj.vars ?? {};
33
+ // 2026-09-17: per-instance event-handler functions (see walk.js's
34
+ // matching addition, and lstnrs.js's evalEvAttr) — a subview's own
35
+ // `function bump(){}` still becomes a real `window.bump` too (plain
36
+ // <script>-tag semantics, unchanged), but multiple mounted
37
+ // instances of the same subview file all define a same-named
38
+ // function, so window.bump silently becomes whichever instance's
39
+ // script ran last — every instance's @click ended up calling THAT
40
+ // one instance's handler regardless of which was actually clicked.
41
+ // .fns is this instance's own, unambiguous copy.
42
+ this.fns = obj.fns ?? {};
32
43
  this.rvs = obj.rvs ?? {};
33
44
  this._pv = obj._pv ?? null;
34
45
  this.mx = obj.mx ?? [];
@@ -110,6 +121,67 @@ function reportLumenError(info) {
110
121
 
111
122
  function _x(_x) {
112
123
  var currPath = [];
124
+ // 2026-09-17: generalized from a position-specific check
125
+ // (`currPath[1] == "vars"`, only ever matched a flat `_vt.<root>.vars`
126
+ // write) to a real owner-walk — needed for per-instance subview
127
+ // scoping (`_vt.View.views[i].vars`, and its nested form
128
+ // `_vt.View.views[0].views[2].vars`), where "vars" can sit at any
129
+ // depth. `path` is currPath *without* the trailing "vars" segment.
130
+ // `Global` is a real special case, not a bug in this generalization:
131
+ // it has no `._re` of its own (see the object literal below — just
132
+ // `{vars:{}}`) since Global-var writes need to re-render whichever
133
+ // view is *currently* looking at that value, exactly like the
134
+ // pre-existing behavior this preserves. Everything else (View itself,
135
+ // or any views[i]/views[i].views[j]/... chain) is walked against the
136
+ // raw (non-proxied) tree — using the outer `_x` param directly, not
137
+ // the `_vt` proxy — so this traversal itself can't re-enter these
138
+ // traps and corrupt the in-flight currPath.
139
+ function _dispatchVarsUpdate(key) {
140
+ // "vars" can be anywhere in currPath, not just at index 1 — find
141
+ // it. The var name to update() is whatever comes right after it
142
+ // in currPath (e.g. `_vt.View.vars["obj"].nested = x` — the write
143
+ // itself is on "obj"'s own proxy, so `key` here is "nested", but
144
+ // the reactive var is "obj", one segment before it in currPath);
145
+ // if "vars" is the *last* segment, there's no next segment yet —
146
+ // that means this write IS the var (`_vt.View.vars["x"] = ...`),
147
+ // so `key` itself (the set() trap's own key) is the var name.
148
+ let varsIdx = currPath.indexOf("vars");
149
+ if (varsIdx === -1) return;
150
+ let varName = varsIdx < currPath.length - 1 ? currPath[varsIdx + 1] : key;
151
+ let rootPath = currPath.slice(0, varsIdx);
152
+ // Global has no ._re of its own (see its plain {vars:{}} literal
153
+ // below) — a Global write always re-renders whichever view is
154
+ // currently looking at it, exactly like the pre-existing behavior
155
+ // this preserves, for both the flat and nested-property case.
156
+ if (rootPath[0] === "Global") {
157
+ // 2026-09-18, real gap found and fixed: this file's own design
158
+ // comment above _vt's declaration says a Global var should be
159
+ // usable by "ordinary view code" just by referencing the
160
+ // name — but nothing ever made that true for a VIEW's own
161
+ // <script> statements (as opposed to mustaches/:if conditions,
162
+ // which already fall back to _vt.Global.vars via getVal()/
163
+ // evalExp()'s own merge). A view's compiled script only
164
+ // rewrites references to names IT ITSELF declares — it has no
165
+ // way to know index.js declared some other name as Global, so
166
+ // a bare reference to it (e.g. `rx.post(...)` where `rx =
167
+ // new RX(...)` was set in index.js's init()) resolved via the
168
+ // real JS scope chain, which only ever finds a real `window.rx`
169
+ // if something put it there — nothing did. Mirroring every
170
+ // Global write onto `window[varName]` too makes plain bare-
171
+ // identifier references resolve correctly and naturally,
172
+ // with zero per-script rewriting needed.
173
+ try {
174
+ if (typeof window !== 'undefined') window[varName] = _x.Global.vars[varName];
175
+ } catch (e) { }
176
+ if (_vt.View._re) _vt.View._re.update(varName);
177
+ return;
178
+ }
179
+ let owner = _x;
180
+ for (let i = 0; i < rootPath.length && owner; i++) {
181
+ owner = owner[rootPath[i]];
182
+ }
183
+ if (owner && owner._re) owner._re.update(varName);
184
+ }
113
185
  const handler = {
114
186
  get(target, key) {
115
187
  if (key == "__isProxy") return true;
@@ -136,31 +208,12 @@ function _x(_x) {
136
208
  // var _View = currPath[1] == "views" ? currPath[2] : "";
137
209
 
138
210
  try {
139
- // cl(["hiiiiii", currPath, key, value]);
140
211
  // `_vt.View._re` genuinely doesn't exist yet during
141
212
  // index.js's own bootstrap (before any view has mounted —
142
213
  // see index-bootstrap.js) — that's the normal case now,
143
- // not an error, so it's guarded here rather than left to
144
- // throw into the catch below on every such write.
145
- if (currPath.length == 2 && currPath[1] == "vars") {
146
- if (_vt.View._re) _vt.View._re.update(key);
147
- } else if (currPath.length > 2 && currPath[1] == "vars") {
148
- let ky = clone(currPath[2]);
149
- if (_vt.View._re) _vt.View._re.update(ky);
150
- }
151
- // if (currPath.length) {
152
- // _vt.View._re.set(key, value);
153
- // }
154
- // cl(currPath, _View);
155
- // eval("var kira = " + "_vt." + currPath.join("."));
156
- // if (kira.__isProxy) {
157
- // currPath.push(key);
158
- // eval("kira = " + "_vt." + currPath.join("."));
159
- // }
160
-
161
- // if (kira) {
162
- // // if (_el.data('_re')) _el.data('_re').set(key, value);
163
- // }
214
+ // not an error, so it's guarded (inside _dispatchVarsUpdate)
215
+ // rather than left to throw into the catch below.
216
+ _dispatchVarsUpdate(key);
164
217
  } catch (e) {
165
218
  cl(e);
166
219
  }
@@ -175,31 +228,7 @@ function _x(_x) {
175
228
  delete target[key];
176
229
 
177
230
  try {
178
- // cl(["hiiiiii", currPath, key, value]);
179
- // `_vt.View._re` genuinely doesn't exist yet during
180
- // index.js's own bootstrap (before any view has mounted —
181
- // see index-bootstrap.js) — that's the normal case now,
182
- // not an error, so it's guarded here rather than left to
183
- // throw into the catch below on every such write.
184
- if (currPath.length == 2 && currPath[1] == "vars") {
185
- if (_vt.View._re) _vt.View._re.update(key);
186
- } else if (currPath.length > 2 && currPath[1] == "vars") {
187
- let ky = clone(currPath[2]);
188
- if (_vt.View._re) _vt.View._re.update(ky);
189
- }
190
- // if (currPath.length) {
191
- // _vt.View._re.set(key, value);
192
- // }
193
- // cl(currPath, _View);
194
- // eval("var kira = " + "_vt." + currPath.join("."));
195
- // if (kira.__isProxy) {
196
- // currPath.push(key);
197
- // eval("kira = " + "_vt." + currPath.join("."));
198
- // }
199
-
200
- // if (kira) {
201
- // // if (_el.data('_re')) _el.data('_re').set(key, value);
202
- // }
231
+ _dispatchVarsUpdate(key);
203
232
  } catch (e) {
204
233
  cl(e);
205
234
  }
@@ -240,9 +269,27 @@ function _x(_x) {
240
269
  // its vars (V1's documented convention for app-wide state like `isAdmin`)
241
270
  // need to outlive any single view. concatVarsAtLevel() below merges it into
242
271
  // every scope lookup so ordinary view code can just reference the name.
272
+ // 2026-09-17, real bug found and fixed: `.fns` was missing here (only
273
+ // `.vars` existed) — `walk.js`'s `_fnRegisterStatement()` (added for
274
+ // per-instance subview scoping, see the per-instance-scoping work above)
275
+ // unconditionally builds `<targetRoot>.fns["name"] = name;` for EVERY
276
+ // top-level function declaration, regardless of target — including
277
+ // `targetKey: "Global"`, used to compile index.js/project scripts. Any
278
+ // real project with so much as one function declaration in index.js (an
279
+ // extremely common pattern) crashed with "Cannot set properties of
280
+ // undefined (setting '<fnName>')" the moment that line ran. Nearly
281
+ // invisible in `lm serve` dev mode — index-bootstrap.js's synchronous XHR
282
+ // fetch-and-eval of the compiled index.js is wrapped in a try/catch that
283
+ // only `console.log`s the failure (never `console.error`/throws), so nothing
284
+ // looked broken as long as the crash landed after anything load-bearing
285
+ // (Reactor()) in that same script. Fatal in a real `lm build` production
286
+ // bundle, where the compiled index.js is concatenated inline, unguarded —
287
+ // an uncaught exception there aborts every remaining statement in the
288
+ // whole bundle file, breaking the entire app on every real production
289
+ // site with this pattern. Found deploying lumenjs.com's own real build.
243
290
  let _vt = _x({
244
291
  "View": new _v({}),
245
- "Global": { "vars": {} }
292
+ "Global": { "vars": {}, "fns": {} }
246
293
  });
247
294
 
248
295
  class _lm {
@@ -388,7 +435,6 @@ class _lm {
388
435
  } else {
389
436
  effect.nd.setAttribute(effect.name, this.render(effect));
390
437
  if (effect.name == "view") {
391
- cl("We Must Trigger View Change", effect, this.render(effect));
392
438
  effect.nd.subPath = this.render(effect);
393
439
  renderView(this.render(effect), true, {});
394
440
  }
@@ -407,6 +453,19 @@ class _lm {
407
453
  this.updateCXRs();
408
454
  this.updateLXRs();
409
455
  this.updateVXRs();
456
+
457
+ // 2026-09-18: same Reactor({tick}) fix as update() below — also
458
+ // fired on the very first render (not just subsequent reactive
459
+ // updates), since a real tick() use (e.g. hljs.highlightAll())
460
+ // needs to run on initial content too, not just after the first
461
+ // change.
462
+ if (typeof appSettings !== 'undefined' && appSettings && typeof appSettings.tick === 'function') {
463
+ try {
464
+ appSettings.tick();
465
+ } catch (e) {
466
+ cl(e);
467
+ }
468
+ }
410
469
  }
411
470
 
412
471
  chainConnected(cx) {
@@ -423,13 +482,25 @@ class _lm {
423
482
  let subsNames = [];
424
483
  for (let i = 0; i < this.view.views.length; i++) {
425
484
  const _view = this.view.views[i];
426
- cl("We Must Render A SubView", _view, subsNames.includes(_view.subPath), subsNames, _view.subPath);
427
485
  if (!subsNames.includes(_view.subPath)) subsNames.push(_view.subPath);
428
486
  }
429
487
  // cl(subsNames);
488
+ // 2026-09-17: real bug found and fixed — this used to always call
489
+ // the plain, no-args renderView(n, true, {}), which only ever
490
+ // searches _vt.View.views (the MAIN view's own array). That's
491
+ // correct when `this` IS the main view, but updateVXRs() also
492
+ // runs on every SUBVIEW instance's own _re (renderAll() calls it
493
+ // unconditionally) — for a subview that itself hosts further
494
+ // subviews, `this.view.views` holds THOSE, not the main view's,
495
+ // so the old call could never find them: nested subviews never
496
+ // rendered at all. Passing this.view.views + this.view.scopePath
497
+ // (set once, in renderHST(), when this instance itself was
498
+ // rendered — ["View"] for the main view, unchanged) lets
499
+ // renderView() search the RIGHT array and address the RIGHT
500
+ // nested scope regardless of how deep this instance is.
430
501
  for (let i = 0; i < subsNames.length; i++) {
431
502
  const n = subsNames[i];
432
- renderView(n, true, {});
503
+ renderView(n, true, {}, 'views', this.view.views, this.view.scopePath || ["View"]);
433
504
  }
434
505
  }
435
506
 
@@ -531,7 +602,6 @@ class _lm {
531
602
  } else {
532
603
  effect.nd.setAttribute(effect.name, this.render(effect));
533
604
  if (effect.name == "view") {
534
- cl("We Must Trigger View Change", effect, this.render(effect));
535
605
  effect.nd.subPath = this.render(effect);
536
606
  renderView(this.render(effect), true, {});
537
607
  }
@@ -551,6 +621,24 @@ class _lm {
551
621
  const sbscr = this.sbscrbs[sbscsi];
552
622
  sbscr._re.update(k);
553
623
  }
624
+
625
+ // 2026-09-18: Reactor({tick}) — documented ("runs after every
626
+ // re-render, anywhere", lumenjs-spec.md §5.4/llms.md §3) but never
627
+ // actually called anywhere in this runtime — see this fix's fuller
628
+ // comment at Reactor()'s own definition (vendor/reconnecting-
629
+ // websocket.js) for how this was found. update() is the real
630
+ // per-reactive-change render point (unlike renderAll(), which only
631
+ // ever runs once per _lm instance, at initial mount) — this is
632
+ // where "after every re-render, anywhere" actually lives. Guarded
633
+ // on appSettings existing since update() can fire during index.js's
634
+ // own bootstrap, before Reactor() has run.
635
+ if (typeof appSettings !== 'undefined' && appSettings && typeof appSettings.tick === 'function') {
636
+ try {
637
+ appSettings.tick();
638
+ } catch (e) {
639
+ cl(e);
640
+ }
641
+ }
554
642
  }
555
643
 
556
644
  async updateLXRs(k) {
@@ -632,12 +720,61 @@ class _lm {
632
720
  vx['value'] = marray[index]['value'];
633
721
  } else {
634
722
  if (forX['as']['k']) vx[forX['as']['k']] = marray[index];
723
+ // 2026-09-17, real bug found and fixed: a bare
724
+ // `:for="items"` (no `as item` alias) is
725
+ // documented — and was V1's real, working
726
+ // behavior — to spread each item's own object
727
+ // keys directly into loop scope, so
728
+ // `items = [{number:1}]` lets a mustache read
729
+ // `{{number}}` with no alias at all. Nothing in
730
+ // this branch ever did that spread — `vx` had
731
+ // no alias key AND no spread keys, so a bare
732
+ // mustache like this always evaluated as an
733
+ // undefined identifier and silently rendered
734
+ // blank. Only spread for a plain-object item —
735
+ // a primitive (string/number) array item has no
736
+ // keys to spread and is read via the untouched
737
+ // alias path instead.
738
+ else if (marray[index] && typeof marray[index] === 'object') {
739
+ for (const k2 in marray[index]) {
740
+ if (Object.prototype.hasOwnProperty.call(marray[index], k2)) {
741
+ vx[k2] = marray[index][k2];
742
+ }
743
+ }
744
+ }
635
745
  }
636
746
  // cl(forX, vx);
637
747
  // cl("VXXXX", vx, this);
638
748
 
639
749
  if (forIf) {
640
- let isTrue = this.evalExp(forIf, vx);
750
+ // 2026-09-17, real bug found and fixed:
751
+ // evalExp()'s 2nd param is a NAMES array to
752
+ // look up in _vt.View.vars/_vt.Global.vars (see
753
+ // its other real call site, `this.evalExp(cx.content,
754
+ // this.reactiveVariables)`) — but `vx` here is
755
+ // a VALUES object (the just-built loop-item
756
+ // scope), not a names array. `vars.length` on a
757
+ // plain object is undefined, so evalExp's own
758
+ // `for (i=0; i<vars.length; i++)` loop never
759
+ // ran even once — every `:for-if` condition
760
+ // referencing the loop item (`item.value > 10`,
761
+ // the exact documented example) always
762
+ // evaluated against an empty scope and either
763
+ // threw (caught, filtered out) or read
764
+ // `undefined`. evalExp() already has a real,
765
+ // working mechanism for exactly this — it
766
+ // merges `this.vrs` (the loop-scope override
767
+ // object) at the highest priority — so route
768
+ // through that instead of a second, broken
769
+ // parameter shape.
770
+ let _prevVrs = this.vrs;
771
+ this.vrs = vx;
772
+ let isTrue;
773
+ try {
774
+ isTrue = this.evalExp(forIf, []);
775
+ } finally {
776
+ this.vrs = _prevVrs;
777
+ }
641
778
  if (!isTrue) continue;
642
779
  }
643
780
 
@@ -670,7 +807,29 @@ class _lm {
670
807
 
671
808
  // cl("ATR", arrayToRender);
672
809
  let oldATR = cx.atr;
673
- cx.atr = arrayToRender;
810
+ // 2026-09-17, real bug found and fixed: cx.atr used to
811
+ // store a reference to arrayToRender's own item objects,
812
+ // not an independent snapshot — arrayToRender's items are
813
+ // the actual current elements of the reactive array (e.g.
814
+ // _vt.View.vars["features"]), so a completely ordinary
815
+ // update pattern (mutate an item in place inside .map(),
816
+ // return the same reference — `f.done = !f.done; return
817
+ // f;`, not "rebuild a new object") left oldATR and the
818
+ // next render's arrayToRender pointing at the exact same,
819
+ // already-mutated objects by the time compareArrays()
820
+ // ran. deepCompare()'s JSON.stringify comparison was
821
+ // never wrong — it was comparing an object against
822
+ // itself, so every 'update' action was silently lost and
823
+ // the DOM never re-rendered. clone() (already used a few
824
+ // lines above for `vals`) gives cx.atr a true independent
825
+ // snapshot instead. Found clicking a real @click-toggled
826
+ // :for item on lumenjs.com's own real V2 homepage — no
827
+ // existing test used an in-place item mutation, only
828
+ // whole-array reassignment with new items (see render-
829
+ // hooks.integration.test.js's `images = [...,'c']`),
830
+ // which never exposed this since a length change always
831
+ // hits compareArrays()'s add/remove branches regardless.
832
+ cx.atr = clone(arrayToRender);
674
833
  // cl(oldATR, arrayToRender);
675
834
 
676
835
  const actions = this.compareArrays(oldATR, arrayToRender);
@@ -924,6 +1083,21 @@ class _lm {
924
1083
  vars[ky] = this.vrs[ky]
925
1084
  }
926
1085
  }
1086
+ // 2026-09-17: per-instance subview scoping — a subview's own
1087
+ // declared vars live on `this.view.vars` (wired up in
1088
+ // renderHST()/renderView() to be the SAME object its compiled
1089
+ // script's `_vt.View.views[i].vars[...]` writes reach), not
1090
+ // in the flat `_vt.View.vars` checked above (that's still
1091
+ // only ever the MAIN view). Checked last/highest-priority,
1092
+ // same shadowing shape as the `this.vrs` merge just above —
1093
+ // an instance's own var wins over anything it inherited.
1094
+ if (this.view && this.view.vars) {
1095
+ for (const ky in this.view.vars) {
1096
+ if (Object.prototype.hasOwnProperty.call(this.view.vars, ky)) {
1097
+ vars[ky] = this.view.vars[ky];
1098
+ }
1099
+ }
1100
+ }
927
1101
  } catch (e) {
928
1102
  cl(e);
929
1103
  }
@@ -1157,16 +1331,97 @@ class _lm {
1157
1331
  createEl(tag, attrs, children, events, doc) {
1158
1332
  const _el = document.createElement(tag);
1159
1333
 
1334
+ // 2026-09-17: every element knows which _lm instance rendered it —
1335
+ // needed so a click on it can resolve its @click handler against
1336
+ // the RIGHT instance's own .fns (see lstnrs.js's evalEvAttr), and
1337
+ // (below) so an @init handler on a subview mount resolves against
1338
+ // ITS creator's own functions/scope, not window. Stamped before
1339
+ // anything below that might need it. Non-enumerable: real bug
1340
+ // found and fixed — an ordinary DOM element has no own enumerable
1341
+ // properties by default, so JSON.stringify(someElement) safely
1342
+ // produced "{}" everywhere this codebase already did that (e.g. a
1343
+ // real test asserting on JSON.stringify(view._slots), real DOM
1344
+ // nodes). An ENUMERABLE back-reference here closes a genuine
1345
+ // cycle (element -> _ownerRe -> that instance's own _RealDOM ->
1346
+ // the same element) and breaks every one of those call sites. Defined this way
1347
+ // instead of a plain `=` assignment so it's reachable
1348
+ // (`el._ownerRe`) but invisible to JSON.stringify/for-in/
1349
+ // Object.keys, matching how a parent-pointer is normally done.
1350
+ Object.defineProperty(_el, '_ownerRe', { value: this, enumerable: false, configurable: true, writable: true });
1351
+
1160
1352
  _el.isSub = false;
1161
1353
  if (attrs.hasOwnProperty('view')) {
1162
1354
  _el.isSub = true;
1163
1355
  _el.subPath = attrs['view'];
1356
+ // 2026-09-17: per-instance subview scoping. `views[]` used to
1357
+ // hold just the raw element — every instance of the same
1358
+ // subview file shared one flat _vt.View.vars, so two mounts of
1359
+ // the same .view file collided on the same variable slots (a
1360
+ // real, confirmed bug — clicking one instance changed a value
1361
+ // neither instance's DOM reflected). DOM elements already
1362
+ // carry arbitrary extra JS properties throughout this file
1363
+ // (.isSub, .subPath, .events) — .vars/.views here follow that
1364
+ // same convention rather than a new parallel structure. .vars
1365
+ // is this instance's own scope, addressed at
1366
+ // _vt.View.views[i].vars once renderHST() wires up the path
1367
+ // (see renderView()); .views lets this instance itself host
1368
+ // further nested subviews the same way.
1369
+ _el.vars = {};
1370
+ _el.views = [];
1371
+ _el.fns = {};
1372
+ // 2026-09-17: `@init="fn"` — the actual, decided mechanism
1373
+ // for giving a subview mount its own data at creation time
1374
+ // (replaces an earlier, narrower `:data="name"` attempt —
1375
+ // reusing the existing @before-render/@after-render hook
1376
+ // machinery instead of a bespoke attribute, per real
1377
+ // discussion of real use cases: a subview file mounted
1378
+ // multiple times needing distinct config per instance — the
1379
+ // same widget showing a different crypto coin per mount, or
1380
+ // a :for item needing its own data slice). `fn`'s return
1381
+ // value (a plain object) is merged into this instance's own
1382
+ // `.vars`, so it can hand over multiple named values at once
1383
+ // — not tied to matching a parent variable's name the way
1384
+ // `:data` was. Reads an ancestor's own variable by name
1385
+ // still needs no special syntax at all (getVal()/evalExp()'s
1386
+ // existing fallthrough already covers that); @init is for
1387
+ // data that's computed/selected specifically for this one
1388
+ // mount. fireRenderHook() needed a real fix first (used to
1389
+ // discard its handler's return value entirely) — see its own
1390
+ // comment. Deliberately one-way — a subview writing back to
1391
+ // a PARENT variable (a steps-form wizard sharing accumulated
1392
+ // state across steps, for instance) is a real, different,
1393
+ // not-yet-designed mechanism, not what this covers.
1394
+ //
1395
+ // Calls evalEvAttr() directly rather than fireRenderHook()
1396
+ // (which every other @before-render/@after-render call site
1397
+ // uses): createEl() itself isn't async, and this result has
1398
+ // to be ready synchronously, before the subview actually
1399
+ // renders (a later, separate, real async call). An @init
1400
+ // handler that returns a Promise can't be awaited here —
1401
+ // deliberately unsupported for now, not silently broken:
1402
+ // only a plain object return is used.
1403
+ if (doc && doc.evs && doc.evs.hasOwnProperty('@init')) {
1404
+ let _initAttr = doc.evs['@init'];
1405
+ if (_initAttr) {
1406
+ // 3rd arg: the CALLING scope's own loop-local bindings
1407
+ // (e.g. `item` from `:for="items as item"`) — these
1408
+ // aren't real JS variables an @init handler could
1409
+ // otherwise close over (it's an ordinary top-level
1410
+ // function, not defined inside the loop), so without
1411
+ // this there'd be no way for it to know which
1412
+ // iteration it's being called for at all.
1413
+ let _initResult = evalEvAttr(_initAttr, { cType: 'init' }, $(_el), 'init', this.vrs);
1414
+ if (_initResult && typeof _initResult === 'object' && typeof _initResult.then !== 'function') {
1415
+ Object.assign(_el.vars, _initResult);
1416
+ }
1417
+ }
1418
+ }
1164
1419
  this.view.views.push(_el);
1165
1420
  }
1166
1421
 
1167
1422
  _el.events = {};
1168
1423
  for (const prop in attrs) {
1169
- if (prop == "view" || prop == ":if" || prop == ":else-if" || prop == ":else" || prop == ":for" || prop == ":for-limit" || prop == ":for-offset" || prop == ":for-if") continue;
1424
+ if (prop == "view" || prop == ":data" || prop == ":if" || prop == ":else-if" || prop == ":else" || prop == ":for" || prop == ":for-limit" || prop == ":for-offset" || prop == ":for-if") continue;
1170
1425
  try {
1171
1426
  // cl("DD", doc);
1172
1427
  let val = (doc && doc.ax.hasOwnProperty(prop)) ? "" : attrs[prop];
@@ -1270,6 +1525,39 @@ class _lm {
1270
1525
  ? _vt.View.vars[__name]
1271
1526
  : _vt.Global.vars[__name];
1272
1527
  }
1528
+ // 2026-09-17, real bug found and fixed: getVal() (mustache
1529
+ // text, e.g. {{f.name}}) has always merged this.vrs in after
1530
+ // the View/Global loop above — the local :for-loop-scoped
1531
+ // binding (f, k, v, index — set via tx._re.vrs = vx, see the
1532
+ // :for update path above) correctly shadows/supplies names a
1533
+ // view's own script never declares. evalExp() (used for
1534
+ // :if="f.done"/:else-if conditions) never had the same merge
1535
+ // — any :if/:else-if condition referencing a :for loop
1536
+ // variable's property could never evaluate correctly, in any
1537
+ // LumenJS V2 project, not just this page. It "looked" correct
1538
+ // on first render only by coincidence: :else isn't gated by
1539
+ // evalExp at all, so an all-false-by-default loop (every item
1540
+ // starting unchecked) rendered the same whether the condition
1541
+ // genuinely evaluated false or silently threw and was caught
1542
+ // below. Found clicking a real @click-toggled :for item on
1543
+ // lumenjs.com's own real V2 homepage.
1544
+ for (const ky in this.vrs) {
1545
+ if (Object.prototype.hasOwnProperty.call(this.vrs, ky)) {
1546
+ rvars[ky] = this.vrs[ky];
1547
+ }
1548
+ }
1549
+ // 2026-09-17: same per-instance subview merge as getVal()
1550
+ // above — a subview's own vars live on this.view.vars, not
1551
+ // the flat _vt.View.vars checked above (always the main
1552
+ // view). See getVal()'s matching comment for the full
1553
+ // rationale.
1554
+ if (this.view && this.view.vars) {
1555
+ for (const ky in this.view.vars) {
1556
+ if (Object.prototype.hasOwnProperty.call(this.view.vars, ky)) {
1557
+ rvars[ky] = this.view.vars[ky];
1558
+ }
1559
+ }
1560
+ }
1273
1561
  } catch (e) {
1274
1562
  cl(e);
1275
1563
  }
@@ -1615,6 +1903,22 @@ class _lm {
1615
1903
  }
1616
1904
  (par.view._slots ?? (par.view._slots = {}))[slotName] = childs;
1617
1905
  }
1906
+ } else if (doc.attrs && doc.attrs.hasOwnProperty('tpl') && !doc.attrs.hasOwnProperty(':for')) {
1907
+ // 2026-09-17: `tpl="name"` used WITHOUT `:for=` on the
1908
+ // same element — the real, documented (lumenjs-
1909
+ // spec.md §4.3) shape always pairs the two, and only
1910
+ // makes sense paired: a template renders inside
1911
+ // whatever repeated context it's used from (see
1912
+ // renderSection()'s own tpl handling, for the
1913
+ // WITH-:for case — an element carrying both never
1914
+ // reaches this branch at all, since :for makes it a
1915
+ // 'sections'-type node the switch above already
1916
+ // dispatched elsewhere). Reported as a real error
1917
+ // rather than silently rendering nothing.
1918
+ reportLumenError({
1919
+ stage: 'tpl',
1920
+ error: new Error('tpl="' + doc.attrs['tpl'] + '" must be used together with :for on the same element — templates only render inside a repeated/list context.'),
1921
+ });
1618
1922
  } else {
1619
1923
  el = par.createEl(doc.name, doc.attrs, [], doc.evs, doc);
1620
1924
 
@@ -1797,7 +2101,26 @@ class _lm {
1797
2101
  }
1798
2102
 
1799
2103
 
1800
- async function renderHST(hst, n, type = 'main', tx, _pv = null) {
2104
+ // 2026-09-17: `scopePath` (new) where in the _vt tree THIS render's own
2105
+ // compiled <script> should write/read its reactive vars. Defaults to
2106
+ // ["View"] (the main view, unchanged behavior for every existing caller).
2107
+ // A subview mount passes its own ["View","views",i] (or the nested form
2108
+ // for a subview hosting a subview) — see renderView()'s isSub branch.
2109
+ // `ownVars`/`ownFns`/`ownViews` (new): for a subview render, the SAME
2110
+ // objects the mount element's `.vars`/`.fns`/`.views` already point to
2111
+ // (not fresh `{}`/`[]`s) — so a write through the compiled script's
2112
+ // `_vt.View.views[i].vars[...]`/`.fns[...]` path (which resolves to that
2113
+ // element's `.vars`/`.fns`) and a read through this instance's own
2114
+ // `getVal()`/`evalExp()` (`this.view.vars`) or `evalEvAttr()`
2115
+ // (`this.view.fns`) see the exact same data. `ownViews` matters for
2116
+ // NESTED subviews specifically: createEl() pushes onto `this.view.views`
2117
+ // while THIS instance is rendering — without linking that to the mount
2118
+ // element's own `.views`, a subview hosting further subviews pushed them
2119
+ // into a freshly-constructed, orphaned array nothing else could ever
2120
+ // find (real bug found testing exactly this — nested subviews mounted
2121
+ // and rendered their own content correctly, but their OWN vars/state
2122
+ // were unreachable from outside, landing in a throwaway array).
2123
+ async function renderHST(hst, n, type = 'main', tx, _pv = null, scopePath = ["View"], ownVars, ownFns, ownViews) {
1801
2124
 
1802
2125
  var reactiveVariables = hst.reactiveVars;
1803
2126
  hst = hst.hst;
@@ -1806,10 +2129,16 @@ async function renderHST(hst, n, type = 'main', tx, _pv = null) {
1806
2129
  "name": n,
1807
2130
  "type": type,
1808
2131
  "hst": hst,
1809
- "vars": tx?.vx,
2132
+ "vars": ownVars ?? tx?.vx,
2133
+ "fns": ownFns,
2134
+ "views": ownViews,
1810
2135
  "rvs": reactiveVariables,
1811
2136
  "_pv": _pv
1812
2137
  }));
2138
+ // 2026-09-17: persisted so THIS instance can later render ITS OWN
2139
+ // subviews (updateVXRs() -> renderView()) at the right nested path —
2140
+ // see both of those for the other half of this.
2141
+ _re.view.scopePath = scopePath;
1813
2142
 
1814
2143
  if (Object.keys(_re._cc).length) {
1815
2144
 
@@ -1882,7 +2211,7 @@ async function renderHST(hst, n, type = 'main', tx, _pv = null) {
1882
2211
  const jses = _re._jj[ky];
1883
2212
  for (let inde = 0; inde < jses.length; inde++) {
1884
2213
  const nd = jses[inde].nd;
1885
- let code = getWatcher(nd._jst, _re.view, reactiveVariables).code;
2214
+ let code = getWatcher(nd._jst, _re.view, reactiveVariables, scopePath).code;
1886
2215
  code = `try { ` + code + ` } catch (e) { reportLumenError({ stage: 'script', error: e }); }`;
1887
2216
  // sourceURL: DevTools shows this <script>'s errors against
1888
2217
  // "<viewname>.view" instead of an anonymous VM context.
@@ -1936,8 +2265,16 @@ async function fireRenderHook(cx, n, containerEl, extra) {
1936
2265
  let ev = Object.assign({ cType: n }, extra || {});
1937
2266
  let result = evalEvAttr(attrVal, ev, $(containerEl), n);
1938
2267
  if (result && typeof result.then === 'function') {
1939
- try { await result; } catch (e) { }
2268
+ // 2026-09-17: this used to just await-and-discard. @init (new,
2269
+ // subview mounts) needs the handler's actual return value — its
2270
+ // whole purpose is "compute this instance's own initial data" —
2271
+ // so a Promise result is now awaited AND returned, not thrown
2272
+ // away. Every existing @before-render/@after-render caller
2273
+ // already ignores this function's return value (none of them
2274
+ // needed it), so this is additive, not a behavior change for them.
2275
+ try { return await result; } catch (e) { return undefined; }
1940
2276
  }
2277
+ return result;
1941
2278
  }
1942
2279
 
1943
2280
  // 2026-09-16: V1's real automatic plugin-init pass — [sl] (Select2),
@@ -2126,7 +2463,45 @@ async function renderSection(tx, doc, par, k, sectionsData) {
2126
2463
 
2127
2464
  var hst = doc.children;
2128
2465
 
2129
- let _re = await renderHST({ hst, mxes: [] }, tx.key, 'section', tx, par);
2466
+ // 2026-09-17: `tpl="name"` on a :for element (`<li :for="items as
2467
+ // item" tpl="row"></li>`, the real, documented — lumenjs-spec.md
2468
+ // §4.3 — shape; never built in V2 until now). A :for element's own
2469
+ // `doc.children` is empty (there's nothing between its tags in the
2470
+ // source), which is exactly what's rendered per item above — so
2471
+ // this substitutes the template file's own HST as this section's
2472
+ // children instead, walked by the SAME engine instance rendering
2473
+ // everything else here (this section already has its own vx/vrs —
2474
+ // no separate scope, no isolated instance, just this item's content).
2475
+ if (doc.attrs && doc.attrs.hasOwnProperty('tpl')) {
2476
+ let _payload = (typeof _vcD !== 'undefined' && _vcD) || (typeof _vcData !== 'undefined' ? _vcData : undefined);
2477
+ let _tplName = doc.attrs['tpl'];
2478
+ let _tplKey = btoa("src/tpls/" + _tplName + ".tpl");
2479
+ let _tplEntry = _payload && _payload.tpls && _payload.tpls[_tplKey];
2480
+ if (_tplEntry) {
2481
+ hst = _tplEntry.hst;
2482
+ } else {
2483
+ reportLumenError({
2484
+ stage: 'tpl',
2485
+ error: new Error('tpl="' + _tplName + '" — no such file at src/tpls/' + _tplName + '.tpl'),
2486
+ });
2487
+ }
2488
+ }
2489
+
2490
+ // 2026-09-17: real bug found and fixed — a subview mounted INSIDE a
2491
+ // :for item (e.g. <div :for="items as item"><div view="widgets/
2492
+ // card"></div></div>) never worked at all: renderHST() used to always
2493
+ // build a fresh, empty `.views` array for whatever it constructs
2494
+ // (see _v's constructor), so createEl() calls made while rendering
2495
+ // THIS item's own content pushed the subview's mount element onto a
2496
+ // throwaway array nothing else could ever find — renderView()'s
2497
+ // isSub search only ever looks at the ENCLOSING view's real `.views`
2498
+ // (or a nested subview's own, itself wired the same way). Passing
2499
+ // par's own `.view.views`/`.scopePath` through here makes every
2500
+ // loop item share the SAME real registry as its enclosing view, so
2501
+ // subview mounts inside a loop accumulate into one addressable array
2502
+ // (sequential index, in render order) exactly like a static mount —
2503
+ // same mechanism either way, just a dynamically-arriving index.
2504
+ let _re = await renderHST({ hst, mxes: [] }, tx.key, 'section', tx, par, par?.view?.scopePath || ["View"], undefined, undefined, par?.view?.views);
2130
2505
  // if(par) {
2131
2506
  // _re.reactiveVariables = par.reactiveVariables;
2132
2507
  // }
@@ -2157,7 +2532,15 @@ async function renderSection(tx, doc, par, k, sectionsData) {
2157
2532
  return _re;
2158
2533
  }
2159
2534
 
2160
- async function renderView(n, isSub, d, type = 'views') {
2535
+ // `viewsArr`/`scopeBase` (2026-09-17): which subview registry to search
2536
+ // and which scope path to address matches under — default to the main
2537
+ // view's own (_vt.View.views / ["View"]) for the top-level, non-recursive
2538
+ // caller (ws.js's real navigation, index-bootstrap.js, etc. — all
2539
+ // unaffected, still work exactly as before). A subview instance rendering
2540
+ // ITS OWN subviews (updateVXRs(), above) passes its own `.views`/
2541
+ // `.scopePath` instead, so nested subviews resolve against the RIGHT
2542
+ // array at the RIGHT nested address instead of always the main view's.
2543
+ async function renderView(n, isSub, d, type = 'views', viewsArr, scopeBase = ["View"]) {
2161
2544
  let filePath = "src/views/" + n + ".view"
2162
2545
  if (type == 'layouts') filePath = "src/layouts/" + n + ".layout";
2163
2546
 
@@ -2213,23 +2596,42 @@ async function renderView(n, isSub, d, type = 'views') {
2213
2596
  // return;
2214
2597
 
2215
2598
  if (isSub) {
2599
+ // 2026-09-17: per-instance subview scoping — each matched
2600
+ // element needs ITS OWN index within the search array (not its
2601
+ // position in this filtered-by-subPath list) so its compiled
2602
+ // script rewrites into `<scopeBase>.views[<that exact index>].vars`,
2603
+ // not the flat `_vt.View.vars` every subview used to collide on.
2604
+ // Searches viewsArr (this main view's _vt.View.views by
2605
+ // default — unchanged for the top-level caller) or, when
2606
+ // called recursively from a subview instance's own
2607
+ // updateVXRs(), THAT instance's own `.views` — see
2608
+ // updateVXRs() for why nested subviews need this.
2609
+ let searchArr = viewsArr || _vt.View.views;
2216
2610
  let els = [];
2217
- for (let i = 0; i < _vt.View.views.length; i++) {
2218
- let _el = _vt.View.views[i];
2611
+ for (let i = 0; i < searchArr.length; i++) {
2612
+ let _el = searchArr[i];
2219
2613
  if (_el.__isProxy) _el = _el.target;
2220
- if (_el.subPath == n) els.push(_el);
2614
+ if (_el.subPath == n) els.push({ el: _el, viewsIndex: i });
2221
2615
  }
2222
2616
  if (els.length) {
2223
2617
  // cl(els);
2224
2618
  for (let i = 0; i < els.length; i++) {
2225
- let _re = await renderHST(hst, n, 'sub');
2226
- const el = els[i];
2619
+ const { el, viewsIndex } = els[i];
2620
+ // Create/reuse el.vars BEFORE rendering, so the fresh
2621
+ // _v instance renderHST() constructs uses this EXACT
2622
+ // object as its own .vars — see renderHST()'s ownVars
2623
+ // param comment for why that identity matters.
2624
+ el.vars = el.vars || {};
2625
+ el.views = el.views || [];
2626
+ el.fns = el.fns || {};
2627
+ let _re = await renderHST(hst, n, 'sub', undefined, null, scopeBase.concat(["views", viewsIndex]), el.vars, el.fns, el.views);
2628
+ el._re = _re;
2227
2629
  el.innerHTML = "";
2228
2630
  // cl(el);
2229
2631
  el.append(..._re._RealDOM);
2230
2632
  _re.renderAll();
2231
2633
  }
2232
- // _re._RealDOM.forEach(_el => el[0].appendChild(_el))
2634
+ // _re._RealDOM.forEach(_el => el[0].appendChild(_el))
2233
2635
  // cl(_re);
2234
2636
  }
2235
2637
  } else {