bimba-cli 0.5.7 → 0.5.9

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/serve.js +55 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimba-cli",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/HeapVoid/bimba.git"
package/serve.js CHANGED
@@ -70,10 +70,25 @@ const hmrClient = `
70
70
 
71
71
  function _applyUpdate(file, slots) {
72
72
  _queue = _queue.then(() => _doUpdate(file, slots)).catch(err => {
73
- console.error('[bimba HMR]', err);
73
+ // Safety net: any uncaught failure during HMR → full reload.
74
+ // Better to lose state than to leave a broken page.
75
+ console.error('[bimba HMR] reload due to error:', err);
76
+ location.reload();
74
77
  });
75
78
  }
76
79
 
80
+ // Walk a subtree and call disconnectedCallback on each custom element.
81
+ // Used before destroying inner DOM on the shifted path so imba/web-component
82
+ // teardown logic (event listeners, observers, etc.) runs cleanly.
83
+ function _disconnectDescendants(root) {
84
+ const all = root.querySelectorAll('*');
85
+ for (const el of all) {
86
+ if (el.tagName.includes('-')) {
87
+ try { el.disconnectedCallback && el.disconnectedCallback(); } catch(_) {}
88
+ }
89
+ }
90
+ }
91
+
77
92
  async function _doUpdate(file, slots) {
78
93
  clearError();
79
94
 
@@ -115,11 +130,23 @@ const hmrClient = `
115
130
  // that imba's runtime uses for lifecycle), clear innerHTML,
116
131
  // restore state, re-render. Loses inner DOM state for instances
117
132
  // of the patched tags, but preserves their instance fields.
133
+ // Snapshot child counts of the patched tags BEFORE commit. On the
134
+ // stable path, child count must not grow — if it does, it means slot
135
+ // stabilization failed for this edit and imba's renderer appended
136
+ // fresh children alongside the old ones. That's the duplication bug
137
+ // we cannot recover from in-place → trigger a reload.
138
+ const childSnap = new Map();
139
+ for (const tag of collected) {
140
+ const list = document.querySelectorAll(tag);
141
+ for (const el of list) childSnap.set(el, el.children.length);
142
+ }
143
+
118
144
  if (slots === 'shifted') {
119
145
  for (const tag of collected) {
120
146
  document.querySelectorAll(tag).forEach(el => {
121
147
  const state = {};
122
148
  for (const k of Object.keys(el)) state[k] = el[k];
149
+ _disconnectDescendants(el);
123
150
  for (const sym of Object.getOwnPropertySymbols(el)) {
124
151
  if (Symbol.keyFor(sym) !== undefined) continue;
125
152
  try { delete el[sym]; } catch(_) {}
@@ -127,12 +154,30 @@ const hmrClient = `
127
154
  el.innerHTML = '';
128
155
  Object.assign(el, state);
129
156
  try { el.render && el.render(); } catch(_) {}
157
+ // Re-fire lifecycle for the top tag itself: imba compiles
158
+ // `def mount` to a `mount()` instance method, and standard
159
+ // connectedCallback may also matter for descendants created
160
+ // by render(). The element is still attached to its parent,
161
+ // so we just call them directly.
162
+ try { el.connectedCallback && el.connectedCallback(); } catch(_) {}
163
+ try { el.mount && el.mount(); } catch(_) {}
130
164
  });
131
165
  }
132
166
  }
133
167
 
134
168
  if (typeof imba !== 'undefined') imba.commit();
135
169
 
170
+ // Stable-path duplication check.
171
+ if (slots !== 'shifted') {
172
+ for (const [el, before] of childSnap) {
173
+ if (el.children.length > before) {
174
+ console.warn('[bimba HMR] slot stabilization failed, reloading');
175
+ location.reload();
176
+ return;
177
+ }
178
+ }
179
+ }
180
+
136
181
  // Smart body dedupe: remove only elements that were ADDED during this
137
182
  // HMR cycle and whose tag already existed in body before. This catches
138
183
  // accidental re-mounts from top-level imba.mount() re-runs, but
@@ -153,28 +198,27 @@ const hmrClient = `
153
198
 
154
199
  // ── Style reaper ───────────────────────────────────────────────────────────
155
200
 
156
- // imba_styles.register inserts <style id="_<hash>"> blocks into <head>.
157
- // We can't intercept register directly (it's inside the imba runtime), so
158
- // we walk all <style> elements with imba-style ids and check whether any
159
- // element in the document still uses one of the classnames declared in
160
- // the stylesheet. If not, the block is dead and gets removed.
201
+ // imba_styles.register inserts <style data-id="<hash>"> blocks into <head>.
202
+ // Walk them, sample a few class selectors, and check whether any element
203
+ // in the document still uses one of those classnames. If not, the block
204
+ // is dead (its tag was hot-replaced with a new content hash and the old
205
+ // classnames are gone from the DOM) remove it.
161
206
  function _reapStyles() {
162
- const styles = document.head.querySelectorAll('style[id^="_"]');
207
+ const styles = document.head.querySelectorAll('style[data-id]');
163
208
  for (const style of styles) {
164
209
  try {
165
210
  const sheet = style.sheet;
166
- if (!sheet) continue;
167
- let used = false;
168
- // Sample a few class selectors and check the document for them.
211
+ if (!sheet || !sheet.cssRules) continue;
169
212
  const probes = [];
170
213
  for (const rule of sheet.cssRules) {
171
214
  if (probes.length >= 4) break;
172
215
  const sel = rule.selectorText;
173
216
  if (!sel) continue;
174
217
  const m = sel.match(/\.(z[a-z0-9_-]+)/i);
175
- if (m) probes.push(m[1]);
218
+ if (m && !probes.includes(m[1])) probes.push(m[1]);
176
219
  }
177
220
  if (!probes.length) continue;
221
+ let used = false;
178
222
  for (const cls of probes) {
179
223
  if (document.querySelector('.' + CSS.escape(cls))) { used = true; break; }
180
224
  }