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.
- package/package.json +1 -1
- package/serve.js +55 -11
package/package.json
CHANGED
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
|
-
|
|
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="
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
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
|
}
|