@stacksjs/stx 0.2.111 → 0.2.112
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/cli.js +663 -590
- package/dist/includes.js +7 -7
- package/dist/pwa.js +441 -368
- package/dist/signals.js +82 -6
- package/dist/ssg.js +422 -349
- package/dist/style-scoping.d.ts +0 -3
- package/dist/style-scoping.js +4 -3
- package/dist/variable-extractor.js +1 -1
- package/dist/web-components/css-scoping.js +33 -0
- package/package.json +5 -5
package/dist/signals.js
CHANGED
|
@@ -2388,6 +2388,13 @@ else {
|
|
|
2388
2388
|
}
|
|
2389
2389
|
|
|
2390
2390
|
let currentElements = [];
|
|
2391
|
+
// One entry per list item, each holding EVERY node that item rendered.
|
|
2392
|
+
// currentElements stays flat for transition snapshots, but the keyed
|
|
2393
|
+
// diff below must pair a key with all of its nodes: a template row
|
|
2394
|
+
// routinely yields several (an element plus its surrounding text nodes),
|
|
2395
|
+
// and indexing a flat list by key position then paired the wrong node
|
|
2396
|
+
// with each key, leaving the list rotated after an update.
|
|
2397
|
+
let currentGroups = [];
|
|
2391
2398
|
let currentKeys = [];
|
|
2392
2399
|
let loadingElement = null;
|
|
2393
2400
|
let emptyElement = null;
|
|
@@ -2529,6 +2536,7 @@ catch (e) {
|
|
|
2529
2536
|
// Dispose scopes registered by items before they leave the DOM (#1727).
|
|
2530
2537
|
currentElements.forEach(e => { disposeSubtreeScopes(e); e.remove(); });
|
|
2531
2538
|
currentElements = [];
|
|
2539
|
+
currentGroups = [];
|
|
2532
2540
|
currentKeys = [];
|
|
2533
2541
|
showLoading();
|
|
2534
2542
|
return;
|
|
@@ -2577,6 +2585,7 @@ catch (e) {
|
|
|
2577
2585
|
// Dispose scopes registered by items before they leave the DOM (#1727).
|
|
2578
2586
|
currentElements.forEach(e => { disposeSubtreeScopes(e); e.remove(); });
|
|
2579
2587
|
currentElements = [];
|
|
2588
|
+
currentGroups = [];
|
|
2580
2589
|
currentKeys = [];
|
|
2581
2590
|
hideEmpty();
|
|
2582
2591
|
return;
|
|
@@ -2617,6 +2626,7 @@ catch (e) {
|
|
|
2617
2626
|
// Dispose scopes registered by items before they leave the DOM (#1727).
|
|
2618
2627
|
currentElements.forEach(e => { disposeSubtreeScopes(e); e.remove(); });
|
|
2619
2628
|
currentElements = [];
|
|
2629
|
+
currentGroups = [];
|
|
2620
2630
|
currentKeys = [];
|
|
2621
2631
|
if (emptyExpr) {
|
|
2622
2632
|
const emptyContent = evalLazy(emptyExpr);
|
|
@@ -2651,11 +2661,12 @@ catch (e) {
|
|
|
2651
2661
|
for (let i = 0; i < currentKeys.length; i++) {
|
|
2652
2662
|
const k = currentKeys[i];
|
|
2653
2663
|
if (!oldKeyMap.has(k)) oldKeyMap.set(k, []);
|
|
2654
|
-
oldKeyMap.get(k).push(
|
|
2664
|
+
oldKeyMap.get(k).push(currentGroups[i] || []);
|
|
2655
2665
|
}
|
|
2656
2666
|
|
|
2657
2667
|
// Build new element list, reusing existing DOM nodes by key
|
|
2658
2668
|
const newElements = [];
|
|
2669
|
+
const newGroups = [];
|
|
2659
2670
|
const usedKeys = new Set();
|
|
2660
2671
|
|
|
2661
2672
|
for (let i = 0; i < list.length; i++) {
|
|
@@ -2663,10 +2674,12 @@ catch (e) {
|
|
|
2663
2674
|
const existing = oldKeyMap.get(key);
|
|
2664
2675
|
|
|
2665
2676
|
if (existing && existing.length > 0 && !usedKeys.has(key)) {
|
|
2666
|
-
// Reuse
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2677
|
+
// Reuse this item's nodes \u2014 move the whole group into position, in
|
|
2678
|
+
// order, so a multi-node row stays contiguous and correctly ordered.
|
|
2679
|
+
const group = existing.shift();
|
|
2680
|
+
group.forEach(el => parent.insertBefore(el, placeholder));
|
|
2681
|
+
newElements.push(...group);
|
|
2682
|
+
newGroups.push(group);
|
|
2670
2683
|
usedKeys.add(key);
|
|
2671
2684
|
// Update the item signal so bindings re-evaluate with new data.
|
|
2672
2685
|
// This is the fix for #1669 \u2014 without this, reused elements show
|
|
@@ -2681,14 +2694,16 @@ catch (e) {
|
|
|
2681
2694
|
const elements = createItemElements(list[i], i, key);
|
|
2682
2695
|
elements.forEach(el => { parent.insertBefore(el, placeholder); if (tgReady) tgEnter(el, tgName); });
|
|
2683
2696
|
newElements.push(...elements);
|
|
2697
|
+
newGroups.push(elements);
|
|
2684
2698
|
}
|
|
2685
2699
|
}
|
|
2686
2700
|
|
|
2687
2701
|
// Remove old elements whose keys are no longer in the list.
|
|
2688
2702
|
// Dispose any scopes registered inside removed items before the
|
|
2689
2703
|
// remove() call (#1727).
|
|
2690
|
-
for (const [key,
|
|
2704
|
+
for (const [key, groups] of oldKeyMap) {
|
|
2691
2705
|
if (!usedKeys.has(key)) {
|
|
2706
|
+
const elements = groups.flat();
|
|
2692
2707
|
// In a transition group, run the leave animation and let it remove the
|
|
2693
2708
|
// node; otherwise remove immediately. Scopes are disposed up-front
|
|
2694
2709
|
// either way (their destroy hooks shouldn't wait on a CSS transition).
|
|
@@ -2698,6 +2713,7 @@ catch (e) {
|
|
|
2698
2713
|
}
|
|
2699
2714
|
|
|
2700
2715
|
currentElements = newElements;
|
|
2716
|
+
currentGroups = newGroups;
|
|
2701
2717
|
currentKeys = newKeys;
|
|
2702
2718
|
|
|
2703
2719
|
// \u2500\u2500 <TransitionGroup> play (#1742) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
@@ -4865,6 +4881,10 @@ else {
|
|
|
4865
4881
|
processedScopes.add(el);
|
|
4866
4882
|
|
|
4867
4883
|
const scopeVars = window.stx._scopes && window.stx._scopes[scopeId];
|
|
4884
|
+
// Record the element this scope is bound to so cleanupContainer can tell a
|
|
4885
|
+
// dead registry entry (element replaced by an SPA swap) from a live one.
|
|
4886
|
+
// See sweepOrphanedScopes / #1775.
|
|
4887
|
+
if (scopeVars) scopeVars.__el = el;
|
|
4868
4888
|
|
|
4869
4889
|
// Deferred island (#1746): its setup script (type="stx/island") hasn't run
|
|
4870
4890
|
// yet, so the scope isn't registered. Arm the hydration trigger anyway \u2014
|
|
@@ -5024,6 +5044,54 @@ catch (e) {
|
|
|
5024
5044
|
}
|
|
5025
5045
|
}
|
|
5026
5046
|
|
|
5047
|
+
// Reclaim registry entries whose element is gone from the document.
|
|
5048
|
+
//
|
|
5049
|
+
// disposeSubtreeScopes only deletes scopes found UNDER the cleanup container,
|
|
5050
|
+
// so two shapes leaked forever across SPA navigation (#1775): components
|
|
5051
|
+
// rendered outside the swap container (an @include in the layout, above
|
|
5052
|
+
// <main>), and elements already detached by the time cleanup ran its
|
|
5053
|
+
// querySelectorAll. Each round-trip re-ran the include scripts under fresh
|
|
5054
|
+
// scope ids while the previous ids stayed in window.stx._scopes \u2014 ~4 orphans
|
|
5055
|
+
// per hop, unbounded, each retaining that scope's vars, signals and closures.
|
|
5056
|
+
//
|
|
5057
|
+
// The sweep is deliberately CONSERVATIVE \u2014 an over-eager delete is the #1737
|
|
5058
|
+
// failure (a scope removed from the registry cannot be recreated: its setup
|
|
5059
|
+
// IIFE ran once at page load), which is strictly worse than a leak. An entry
|
|
5060
|
+
// is dropped only when all three hold:
|
|
5061
|
+
// 1. the runtime actually bound it to an element (__el recorded),
|
|
5062
|
+
// 2. that element is no longer connected to the document, and
|
|
5063
|
+
// 3. neither it nor any ancestor is retained by a live conditional.
|
|
5064
|
+
// Entries with no __el (e.g. a scope whose element still sits inside untouched
|
|
5065
|
+
// <template> content, never inserted) are left alone \u2014 we can't prove they're
|
|
5066
|
+
// dead, and a later clone re-uses that registration.
|
|
5067
|
+
function sweepOrphanedScopes() {
|
|
5068
|
+
if (!window.stx || !window.stx._scopes) return;
|
|
5069
|
+
var scopes = window.stx._scopes;
|
|
5070
|
+
for (var id in scopes) {
|
|
5071
|
+
if (!Object.prototype.hasOwnProperty.call(scopes, id)) continue;
|
|
5072
|
+
var entry = scopes[id];
|
|
5073
|
+
if (!entry) continue;
|
|
5074
|
+
var el = entry.__el;
|
|
5075
|
+
if (!el || el.isConnected) continue;
|
|
5076
|
+
// Detached but RETAINED: :if toggles keep their element (bindIf re-inserts
|
|
5077
|
+
// the SAME node) and :if/:else chains keep their branches. Parent links
|
|
5078
|
+
// inside a detached subtree stay intact, so walking up from the scope
|
|
5079
|
+
// element still finds the conditional that owns it.
|
|
5080
|
+
var retained = false;
|
|
5081
|
+
for (var p = el; p; p = p.parentNode) {
|
|
5082
|
+
if (p.__stx_if_bound || p.__stx_chain_member) { retained = true; break; }
|
|
5083
|
+
}
|
|
5084
|
+
if (retained) continue;
|
|
5085
|
+
if (entry.__destroyCallbacks && Array.isArray(entry.__destroyCallbacks)) {
|
|
5086
|
+
for (var k = 0; k < entry.__destroyCallbacks.length; k++) {
|
|
5087
|
+
try { entry.__destroyCallbacks[k](); }
|
|
5088
|
+
catch (e) { console.warn('[stx] scope destroy error:', e); }
|
|
5089
|
+
}
|
|
5090
|
+
}
|
|
5091
|
+
delete scopes[id];
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
5094
|
+
|
|
5027
5095
|
// ==========================================================================
|
|
5028
5096
|
// Container Cleanup (for SPA navigation)
|
|
5029
5097
|
// ==========================================================================
|
|
@@ -5091,6 +5159,11 @@ catch (e) { console.warn('[stx] destroy hook error:', e); }
|
|
|
5091
5159
|
// through disposeSubtreeScopes so the same walk runs for SPA
|
|
5092
5160
|
// navigation and for :if/:for-driven unmount (see #1727).
|
|
5093
5161
|
disposeSubtreeScopes(container);
|
|
5162
|
+
|
|
5163
|
+
// 4. Reclaim entries the container walk can't see \u2014 components rendered
|
|
5164
|
+
// outside the swap container, or already detached before step 3 ran its
|
|
5165
|
+
// querySelectorAll. Without this the registry grew every navigation (#1775).
|
|
5166
|
+
sweepOrphanedScopes();
|
|
5094
5167
|
}
|
|
5095
5168
|
|
|
5096
5169
|
// Re-initialize components after SPA content swap.
|
|
@@ -5186,6 +5259,9 @@ catch (e) { console.warn('[stx] destroy callback error:', e); }
|
|
|
5186
5259
|
document.body.querySelectorAll('[data-stx-scope]').forEach(function(el) {
|
|
5187
5260
|
var scopeId = el.getAttribute('data-stx-scope');
|
|
5188
5261
|
var scopeVars = window.stx._scopes && window.stx._scopes[scopeId];
|
|
5262
|
+
// Record the bound element for the orphan sweep (#1775) \u2014 see the
|
|
5263
|
+
// DOMContentLoaded walk above for the rationale.
|
|
5264
|
+
if (scopeVars) scopeVars.__el = el;
|
|
5189
5265
|
// Deferred island reached via SPA navigation (#1746): its setup script is
|
|
5190
5266
|
// inert (type="stx/island"), so the scope isn't registered yet. Arm the
|
|
5191
5267
|
// hydration trigger (deferHydration) instead of skipping it \u2014 mirrors the
|