@lmjs/core 2.0.0 → 2.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/src/dom-shim.js CHANGED
@@ -128,11 +128,33 @@
128
128
  return this;
129
129
  };
130
130
  $.fn.get = function (i) { return i === undefined ? toArray(this) : this[i]; };
131
+ // 2026-09-17, found alongside .find()/.is() (same real bug's radio-
132
+ // group branch in checkFormInputs()) — excludes elements matching a
133
+ // raw DOM node, an array of nodes, or a $()-wrapped set; jQuery also
134
+ // supports a selector string here, not needed by anything in this
135
+ // codebase's actual usage (grepped), so not implemented.
136
+ $.fn.not = function (exclude) {
137
+ var excludeArr = exclude instanceof $ ? toArray(exclude) : (Array.isArray(exclude) ? exclude : [exclude]);
138
+ var out = [];
139
+ this.each(function () { if (excludeArr.indexOf(this) === -1) out.push(this); });
140
+ return $(out);
141
+ };
131
142
 
132
143
  $.fn.attr = function (name, value) {
133
144
  if (value === undefined) return this[0] ? this[0].getAttribute(name) : undefined;
134
145
  return this.each(function () { this.setAttribute(name, value); });
135
146
  };
147
+ // 2026-09-17, found in the same real-page click test as .find()/.is()/
148
+ // .not() above — real jQuery distinguishes .prop() (a direct DOM
149
+ // property, e.g. .checked/.disabled/.scrollHeight) from .attr() (the
150
+ // HTML attribute string); the two diverge for exactly the boolean
151
+ // properties this codebase's real usage needs (checkFormInputs()'s
152
+ // "disabled", the vendored search-widget's "disabled", scroll
153
+ // measurements) — grepped actual call sites, not guessed.
154
+ $.fn.prop = function (name, value) {
155
+ if (value === undefined) return this[0] ? this[0][name] : undefined;
156
+ return this.each(function () { this[name] = value; });
157
+ };
136
158
  $.fn.removeAttr = function (name) {
137
159
  return this.each(function () { this.removeAttribute(name); });
138
160
  };
@@ -188,6 +210,15 @@
188
210
  }
189
211
  return this.each(function () { this.innerHTML = value; });
190
212
  };
213
+ // 2026-09-17, found alongside .find()/.is()/.not()/.prop() above —
214
+ // checkFormInputs()'s "data-into" error-message branch calls
215
+ // into.text(...). Not hit by the specific real page that surfaced
216
+ // the others (this codebase's checkbox demo has no data-into), added
217
+ // proactively rather than waiting for a second real crash to find it.
218
+ $.fn.text = function (value) {
219
+ if (value === undefined) return this[0] ? this[0].textContent : '';
220
+ return this.each(function () { this.textContent = value; });
221
+ };
191
222
  $.fn.css = function (name, value) {
192
223
  if (typeof name === 'object') {
193
224
  return this.each(function () {
@@ -306,6 +337,45 @@
306
337
  $.fn.parent = function () { return $(this[0] ? this[0].parentElement : null); };
307
338
  $.fn.next = function () { return $(this[0] ? this[0].nextElementSibling : null); };
308
339
  $.fn.closest = function (sel) { return $(this[0] ? this[0].closest(sel) : null); };
340
+ // 2026-09-17, real bug found and fixed: never implemented at all —
341
+ // vendor/reconnecting-websocket.js's ported V1 form-validation code
342
+ // (checkFormInputs()) calls f.find(...) on the result of .closest(
343
+ // "form"), and .closest() legitimately returns an empty $() when
344
+ // there's no enclosing form (a completely normal case — a single
345
+ // checkbox/input outside a <form>). With .find() missing, that threw
346
+ // "f.find is not a function" the moment ANY input/textarea/select
347
+ // fired a "change" event anywhere on the page, form or no form —
348
+ // found clicking a plain checkbox on lumenjs.com's own real V2
349
+ // homepage. Real jQuery .find() semantics: search descendants of
350
+ // each matched element, union the results (safe/empty on an empty
351
+ // set, exactly what's needed here).
352
+ $.fn.find = function (sel) {
353
+ var out = [];
354
+ this.each(function () {
355
+ var found = this.querySelectorAll(sel);
356
+ for (var i = 0; i < found.length; i++) if (out.indexOf(found[i]) === -1) out.push(found[i]);
357
+ });
358
+ return $(out);
359
+ };
360
+ // 2026-09-17, found alongside .find() (same crash, next line of the
361
+ // same real bug): checkFormInputs() also calls el.is(":checkbox")/
362
+ // ":radio"/":checked" — real CSS pseudo-classes for the former two
363
+ // don't exist (native .matches(":checkbox") throws a SyntaxError,
364
+ // it's a jQuery-only pseudo-selector), so a short, explicit table for
365
+ // the handful actually used across the vendored code (grepped, not
366
+ // guessed) plus a native .matches() fallback for everything else
367
+ // (tag names, real attribute selectors like "[dp0]") — safely false
368
+ // rather than throwing if a selector native matches() can't parse.
369
+ var JQUERY_PSEUDOS = {
370
+ ':checkbox': function (el) { return el.tagName === 'INPUT' && el.type === 'checkbox'; },
371
+ ':radio': function (el) { return el.tagName === 'INPUT' && el.type === 'radio'; },
372
+ ':checked': function (el) { return !!el.checked; },
373
+ };
374
+ $.fn.is = function (sel) {
375
+ if (!this[0]) return false;
376
+ if (JQUERY_PSEUDOS[sel]) return JQUERY_PSEUDOS[sel](this[0]);
377
+ try { return this[0].matches(sel); } catch (e) { return false; }
378
+ };
309
379
  $.fn.ready = function (fn) {
310
380
  if (document.readyState !== 'loading') fn($);
311
381
  else document.addEventListener('DOMContentLoaded', function () { fn($); });
@@ -468,6 +538,103 @@
468
538
  return this;
469
539
  };
470
540
 
541
+ // 2026-09-18, real gap found testing a real migrated V1 project end-to-
542
+ // end: `st()` (vendor/reconnecting-websocket.js's own real, shipped
543
+ // scroll-to helper — not project code, framework code) does
544
+ // `$("html,body").stop().animate({scrollTop: x}, {duration: 400})` —
545
+ // a very common pattern across real V1 pages (called at the top of
546
+ // most real view scripts for smooth-scroll navigation). Neither
547
+ // method existed in the shim at all, throwing "not a function" and
548
+ // aborting the rest of that script on every real page that called it.
549
+ // `scrollTop`/`scrollLeft` are DOM properties, not CSS ones — handled
550
+ // as a special case, matching real jQuery's own animate() behavior;
551
+ // any other property is treated as a numeric CSS property (adding
552
+ // "px" unless it's already a bare number-only string/value, matching
553
+ // jQuery's own unit-inference for a plain number).
554
+ var _animTimers = new WeakMap();
555
+ function _animGetScroll(el, prop) {
556
+ if (el === window) return prop === 'scrollLeft' ? window.pageXOffset : window.pageYOffset;
557
+ return el[prop];
558
+ }
559
+ function _animSetScroll(el, prop, val) {
560
+ if (el === window) {
561
+ if (prop === 'scrollLeft') window.scrollTo(val, window.pageYOffset);
562
+ else window.scrollTo(window.pageXOffset, val);
563
+ } else {
564
+ el[prop] = val;
565
+ }
566
+ }
567
+ // Real jQuery's own cssNumber list — these CSS properties take a bare
568
+ // unitless number, not "Npx" (opacity: "0.5px" is simply invalid and
569
+ // silently ignored by the browser, which is exactly what broke the
570
+ // very first real-content test of this: an opacity fade never moved).
571
+ var _cssNumberProps = { opacity: 1, zIndex: 1, zoom: 1, fontWeight: 1, lineHeight: 1, columnCount: 1, flexGrow: 1, flexShrink: 1, order: 1, widows: 1, orphans: 1 };
572
+ function _animSetStyle(el, prop, val) {
573
+ el.style[prop] = _cssNumberProps[prop] ? val : (val + 'px');
574
+ }
575
+ $.fn.animate = function (props, options) {
576
+ var opts = typeof options === 'number' ? { duration: options } : (options || {});
577
+ var duration = opts.duration == null ? 400 : opts.duration;
578
+ this.each(function () {
579
+ var el = this;
580
+ var isWindowScroll = (el === document.documentElement || el === document.body);
581
+ var target = isWindowScroll ? window : el;
582
+ var start = {};
583
+ var isScrollProp = {};
584
+ for (var prop in props) {
585
+ if (!Object.prototype.hasOwnProperty.call(props, prop)) continue;
586
+ isScrollProp[prop] = (prop === 'scrollTop' || prop === 'scrollLeft');
587
+ if (isScrollProp[prop]) {
588
+ start[prop] = _animGetScroll(isWindowScroll ? window : el, prop);
589
+ } else {
590
+ start[prop] = parseFloat(getComputedStyle(el)[prop]) || 0;
591
+ }
592
+ }
593
+ var existing = _animTimers.get(el);
594
+ if (existing) cancelAnimationFrame(existing);
595
+ if (duration === 0) {
596
+ for (var p in props) {
597
+ var v = parseFloat(props[p]);
598
+ if (isScrollProp[p]) _animSetScroll(isWindowScroll ? window : el, p, v);
599
+ else _animSetStyle(el, p, v);
600
+ }
601
+ if (opts.complete) opts.complete.call(el);
602
+ return;
603
+ }
604
+ var startTime = null;
605
+ function step(ts) {
606
+ if (startTime === null) startTime = ts;
607
+ var elapsed = ts - startTime;
608
+ var t = Math.min(1, elapsed / duration);
609
+ for (var p in props) {
610
+ if (!Object.prototype.hasOwnProperty.call(props, p)) continue;
611
+ var end = parseFloat(props[p]);
612
+ var val = start[p] + (end - start[p]) * t;
613
+ if (isScrollProp[p]) _animSetScroll(isWindowScroll ? window : el, p, val);
614
+ else _animSetStyle(el, p, val);
615
+ }
616
+ if (t < 1) {
617
+ _animTimers.set(el, requestAnimationFrame(step));
618
+ } else {
619
+ _animTimers.delete(el);
620
+ if (opts.complete) opts.complete.call(el);
621
+ }
622
+ }
623
+ _animTimers.set(el, requestAnimationFrame(step));
624
+ });
625
+ return this;
626
+ };
627
+ $.fn.stop = function () {
628
+ this.each(function () {
629
+ var id = _animTimers.get(this);
630
+ if (id) {
631
+ cancelAnimationFrame(id);
632
+ _animTimers.delete(this);
633
+ }
634
+ });
635
+ return this;
636
+ };
637
+
471
638
  global.$ = $;
472
639
  global.jQuery = $;
473
640
  })(typeof window !== 'undefined' ? window : globalThis);
@@ -41,7 +41,13 @@ if (typeof _beaTn === 'undefined' || !_beaTn) {
41
41
  console.log('Failed to load index.js: HTTP ' + _idxXhr.status);
42
42
  }
43
43
  } catch (e) {
44
- console.log('Failed to load index.js: ', e);
44
+ // 2026-09-17: was console.log a real exception partway through
45
+ // index.js (e.g. the _vt.Global.fns crash this session found and
46
+ // fixed) silently looked like nothing happened at all in dev
47
+ // mode, while the same bug was fatal in a real production
48
+ // build (index.js runs inline there, unguarded). A real error
49
+ // here should be as visible as reportLumenError()'s.
50
+ console.error('Failed to load index.js: ', e);
45
51
  }
46
52
  }
47
53
  }
package/src/lstnrs.js CHANGED
@@ -496,9 +496,27 @@ $("body").on("modal-open", "[\\@modal-open]", function (ev, modal) {
496
496
  function evalEvAttr(_v, ev, el, n, arg1, arg2, arg3) {
497
497
  let _fn = _v.split("(")[0];
498
498
  try {
499
- if (typeof window[_v] === "function") {
499
+ // 2026-09-17: prefer the triggering element's OWN rendering instance's
500
+ // function registry (._ownerRe.view.fns, set up by walk.js's
501
+ // _fnRegisterStatement() + _re.js's createEl()/renderHST()) over the
502
+ // plain `window[_fn]` lookup below — real bug found and fixed: every
503
+ // mounted instance of the same subview declaring a same-named handler
504
+ // (e.g. multiple `<div view="widgets/card">` each with their own
505
+ // `function bump(){}`) used to collide on one global, since a plain
506
+ // <script> tag's top-level function declarations all write to the
507
+ // same `window` object — window.bump silently became whichever
508
+ // instance's script last ran, so every instance's @click fired THAT
509
+ // one instance's handler regardless of which was actually clicked.
510
+ // Only applies to the plain-name form (`_v === _fn`, no `(...)`) —
511
+ // a parameterized attribute like `@click="fn(x)"` already falls
512
+ // through to the eval() branch below unchanged, exactly as before.
513
+ let _rawEl = el[0];
514
+ let _ownerFns = _rawEl && _rawEl._ownerRe && _rawEl._ownerRe.view && _rawEl._ownerRe.view.fns;
515
+ let _ownFn = (_v === _fn && _ownerFns && typeof _ownerFns[_fn] === "function") ? _ownerFns[_fn] : null;
516
+ if (_ownFn || typeof window[_v] === "function") {
500
517
  ev["cType"] = n;
501
- var _data = arg3 ? window[_fn](ev, el, arg1, arg2, arg3) : (arg2 ? window[_fn](ev, el, arg1, arg2) : (arg1 ? window[_fn](ev, el, arg1) : window[_fn](ev, el)));
518
+ let _handler = _ownFn || window[_fn];
519
+ var _data = arg3 ? _handler(ev, el, arg1, arg2, arg3) : (arg2 ? _handler(ev, el, arg1, arg2) : (arg1 ? _handler(ev, el, arg1) : _handler(ev, el)));
502
520
  return _data;
503
521
  } else {
504
522
  let _eva = eval(_v);
package/src/walk.js CHANGED
@@ -278,6 +278,28 @@ function getL1Vs(AST, view, vazzz) {
278
278
  }
279
279
 
280
280
  function getWatcher(AST, view, vazzz, targetKey = "View") {
281
+ // 2026-09-17, real bug found and fixed: changeReactiveVarsOccurences()/
282
+ // transformTopLevelDeclarations() mutate the AST IN PLACE (they
283
+ // replace nodes via `parent[prop] = replacement`, not by building a
284
+ // new tree) — harmless as long as a given AST object is only ever
285
+ // rewritten once, which used to always be true. Per-instance subview
286
+ // scoping changed that: renderHST() (_re.js) is called once per
287
+ // subview MOUNT, but all mounts of the same .view file share the
288
+ // exact same parsed `nd._jst` object (parsed once, cached, reused) —
289
+ // so the first instance's rewrite (into e.g. _vt.View.views[0].vars)
290
+ // permanently consumed the raw identifiers, and every subsequent
291
+ // instance's "rewrite" silently found nothing left to rewrite,
292
+ // regenerating instance 0's already-rewritten code unchanged. Found
293
+ // testing 3 real mounted instances of the same subview on lumenjs.com's
294
+ // own V2 homepage — only the first ever got its own correct scope.
295
+ // Cloning here (not at each call site) makes getWatcher() safe to
296
+ // call repeatedly against the same input AST, which is now a real
297
+ // requirement, not just good hygiene. ASTs are plain, JSON-safe data
298
+ // (no functions/circular refs), so JSON round-tripping is a correct,
299
+ // dependency-free deep clone — walk.js has no access to the vendored
300
+ // recursive clone() helper (a different, browser-only bundle file).
301
+ AST = JSON.parse(JSON.stringify(AST));
302
+
281
303
  let Vars = getL1Vs(AST, view, vazzz);
282
304
  AST = Vars['AST'];
283
305
  let varz = Vars['varz'];
@@ -313,11 +335,46 @@ function validateBeforeRewrite(AST, view) {
313
335
  }
314
336
  }
315
337
 
338
+ // 2026-09-17: `targetKey` used to only ever be a plain string ("View" or
339
+ // "Global"), producing a fixed 2-level root (`_vt.View`/`_vt.Global`). Real
340
+ // per-instance subview scoping (`_vt.View.views[i].vars`, and its nested
341
+ // form `_vt.View.views[0].views[2].vars` for a subview hosting a subview)
342
+ // needs a *deeper* chain than a single Identifier can express. Accepts
343
+ // either form now: a bare string is treated as a one-element path (100%
344
+ // unchanged output for every existing caller — "View"/"Global" still build
345
+ // exactly `_vt.View`/`_vt.Global`); an array of segments builds the real
346
+ // chain, a string segment as a non-computed Identifier access, a number
347
+ // segment as a computed numeric-literal access (`.views[3]`).
348
+ function buildTargetRootExpr(targetKey) {
349
+ const segments = Array.isArray(targetKey) ? targetKey : [targetKey];
350
+ let expr = { type: "Identifier", name: "_vt" };
351
+ for (const seg of segments) {
352
+ if (typeof seg === "number") {
353
+ expr = {
354
+ type: "MemberExpression",
355
+ object: expr,
356
+ property: { type: "Literal", value: seg, raw: String(seg) },
357
+ computed: true
358
+ };
359
+ } else {
360
+ expr = {
361
+ type: "MemberExpression",
362
+ object: expr,
363
+ property: { type: "Identifier", name: seg },
364
+ computed: false
365
+ };
366
+ }
367
+ }
368
+ return expr;
369
+ }
370
+
316
371
  // The kept reactive-variable rewrite: turns a reference to a reactive
317
372
  // variable into `_vt.View.vars["<name>"]`, matching `_re.js`'s Proxy target.
318
373
  // `targetKey` (2026-09-14): which root of `_vt` to rewrite into — "View"
319
374
  // (default, unchanged) for .view <script> blocks, "Global" for index.js's
320
375
  // top-level vars, which need to outlive a single view (see _re.js's _vt.Global).
376
+ // Can also be a path array (see buildTargetRootExpr above) for a subview
377
+ // instance's own scope.
321
378
  function changeReactiveVarsOccurences(AST, reactiveVariables, targetKey = "View") {
322
379
  const reactive = new Set(reactiveVariables || []);
323
380
 
@@ -611,17 +668,12 @@ function changeReactiveVarsOccurences(AST, reactiveVariables, targetKey = "View"
611
668
  parent.shorthand = false;
612
669
  }
613
670
 
614
- // Build computed member expression: _vt.View.vars["<name>"]
671
+ // Build computed member expression: _vt.<targetKey>.vars["<name>"]
615
672
  const replacement = {
616
673
  type: "MemberExpression",
617
674
  object: {
618
675
  type: "MemberExpression",
619
- object: {
620
- type: "MemberExpression",
621
- object: { type: "Identifier", name: "_vt" },
622
- property: { type: "Identifier", name: targetKey },
623
- computed: false
624
- },
676
+ object: buildTargetRootExpr(targetKey),
625
677
  property: { type: "Identifier", name: "vars" },
626
678
  computed: false
627
679
  },
@@ -684,12 +736,7 @@ function _reactiveAssignStatement(name, init, targetKey = "View") {
684
736
  object: {
685
737
  type: "MemberExpression",
686
738
  computed: false,
687
- object: {
688
- type: "MemberExpression",
689
- computed: false,
690
- object: { type: "Identifier", name: "_vt" },
691
- property: { type: "Identifier", name: targetKey }
692
- },
739
+ object: buildTargetRootExpr(targetKey),
693
740
  property: { type: "Identifier", name: "vars" }
694
741
  },
695
742
  property: { type: "Literal", value: name }
@@ -699,6 +746,41 @@ function _reactiveAssignStatement(name, init, targetKey = "View") {
699
746
  };
700
747
  }
701
748
 
749
+ // 2026-09-17: registers a top-level function declaration onto this
750
+ // render's own scope (`_vt.<targetKey>.fns["name"] = name`) — ADDITIVE,
751
+ // not a replacement: the function declaration itself is left completely
752
+ // untouched right above this (still a real `window.name` too, the
753
+ // existing, deliberate "functions stay real globals so onclick='fn()'
754
+ // keeps working" design — see compileProjectScript's matching comment).
755
+ // Needed once multiple instances of the same subview can be mounted at
756
+ // once (see _re.js's per-instance scoping work) — every instance
757
+ // declaring a same-named handler collided on the one global function
758
+ // (window.bump became whichever instance's script ran last), so every
759
+ // instance's @click ended up calling THAT one instance's handler
760
+ // regardless of which was actually clicked. lstnrs.js's evalEvAttr()
761
+ // checks this per-instance copy first, before falling back to window.
762
+ function _fnRegisterStatement(name, targetKey) {
763
+ return {
764
+ type: "ExpressionStatement",
765
+ expression: {
766
+ type: "AssignmentExpression",
767
+ operator: "=",
768
+ left: {
769
+ type: "MemberExpression",
770
+ computed: true,
771
+ object: {
772
+ type: "MemberExpression",
773
+ computed: false,
774
+ object: buildTargetRootExpr(targetKey),
775
+ property: { type: "Identifier", name: "fns" }
776
+ },
777
+ property: { type: "Literal", value: name }
778
+ },
779
+ right: { type: "Identifier", name: name }
780
+ }
781
+ };
782
+ }
783
+
702
784
  // Handles any number of declarators in a single top-level statement, not just
703
785
  // one. `var x = 1, y = 2;` with only `x` reactive becomes two statements:
704
786
  // `_vt.View.vars["x"] = 1;` followed by a real `var y = 2;` for the rest —
@@ -709,6 +791,12 @@ function transformTopLevelDeclarations(AST, reactiveVariables, targetKey = "View
709
791
  const newBody = [];
710
792
 
711
793
  for (const stmt of AST.body) {
794
+ if (stmt.type === "FunctionDeclaration" && stmt.id) {
795
+ newBody.push(stmt);
796
+ newBody.push(_fnRegisterStatement(stmt.id.name, targetKey));
797
+ continue;
798
+ }
799
+
712
800
  if (stmt.type !== "VariableDeclaration") {
713
801
  newBody.push(stmt);
714
802
  continue;
@@ -247,6 +247,31 @@ var _ups = [];
247
247
  if (o.Additional) {
248
248
  bea([o.Additional], "ready", function () { });
249
249
  }
250
+ // 2026-09-18, real bug found and fixed: Reactor({init, tick, auth})
251
+ // is documented (lumenjs-spec.md §3, llms.md §3) and real V1 projects
252
+ // depend on it (app-wide bootstrap work in init() — auth/session
253
+ // setup, an SDK instance like `rx = new RX(...)`/`bea = new BEA(...)`,
254
+ // globals defaults; periodic work in tick(), e.g. re-highlighting
255
+ // code blocks after every render) — but nothing in this whole
256
+ // runtime ever read o.init/o.tick/o.auth at all. A real, unrelated-
257
+ // looking function (setAppFuncs(), earlier in this same file) does
258
+ // check appSettings.init — but is itself never called from
259
+ // anywhere in the shipped runtime, dead code calling more dead
260
+ // code. Confirmed via a real migrated V1 project end-to-end:
261
+ // `init()`-set globals (`globals.Settings`, an `rx`/`bea` SDK
262
+ // instance) were silently never set, with no error at all — every
263
+ // reference to them elsewhere threw "not defined" instead, easy to
264
+ // misdiagnose as an unrelated data/scoping bug (which is exactly
265
+ // what this took real, extensive tracing to rule out).
266
+ // init(): synchronous, called once, right here — Reactor(o) itself
267
+ // is always the first thing a real index.js does, before whatever
268
+ // triggers the first render (the ready-handler retry guard
269
+ // elsewhere in this file already waits on appSettings.Base, set
270
+ // just above, so no render can start before this function returns).
271
+ if (o.init && typeof o.init === "function") o.init();
272
+ // tick(): "after every re-render, anywhere" (documented) — hooked
273
+ // into _lm.prototype.renderAll in _re.js instead of duplicated
274
+ // here; see that file's own comment for why there, not here.
250
275
  };
251
276
  });
252
277