bimba-cli 0.5.6 → 0.5.8
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 +116 -52
package/package.json
CHANGED
package/serve.js
CHANGED
|
@@ -25,10 +25,10 @@ const hmrClient = `
|
|
|
25
25
|
//
|
|
26
26
|
const _origDefine = customElements.define.bind(customElements);
|
|
27
27
|
const _classes = new Map(); // tagName → first-registered constructor
|
|
28
|
-
let
|
|
28
|
+
let _collector = null; // when set, captures tag names defined during one HMR import
|
|
29
29
|
|
|
30
30
|
customElements.define = function(name, cls, opts) {
|
|
31
|
-
|
|
31
|
+
if (_collector) _collector.push(name);
|
|
32
32
|
const existing = customElements.get(name);
|
|
33
33
|
if (!existing) {
|
|
34
34
|
_origDefine(name, cls, opts);
|
|
@@ -62,60 +62,124 @@ const hmrClient = `
|
|
|
62
62
|
|
|
63
63
|
// ── HMR update handler ─────────────────────────────────────────────────────
|
|
64
64
|
|
|
65
|
+
// Updates are serialized via a promise queue. Without this, two file edits
|
|
66
|
+
// arriving back-to-back would race on the shared collector and on imba's
|
|
67
|
+
// reconcile loop, with the second update potentially missing tags from
|
|
68
|
+
// the first.
|
|
69
|
+
let _queue = Promise.resolve();
|
|
70
|
+
|
|
65
71
|
function _applyUpdate(file, slots) {
|
|
72
|
+
_queue = _queue.then(() => _doUpdate(file, slots)).catch(err => {
|
|
73
|
+
console.error('[bimba HMR]', err);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function _doUpdate(file, slots) {
|
|
66
78
|
clearError();
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
79
|
+
|
|
80
|
+
// Snapshot direct body children BEFORE importing the new module, so we
|
|
81
|
+
// know which elements pre-existed. After commit we only dedupe NEW
|
|
82
|
+
// elements whose tag also existed before — this preserves legitimate
|
|
83
|
+
// multi-instance roots like toasts and parallel popups, while still
|
|
84
|
+
// catching accidental re-mounts from re-running top-level code.
|
|
85
|
+
const bodyBefore = new Set(document.body.children);
|
|
86
|
+
const tagsBefore = new Set();
|
|
87
|
+
for (const el of bodyBefore) tagsBefore.add(el.tagName.toLowerCase());
|
|
88
|
+
|
|
89
|
+
// Use a local collector instead of a shared variable so concurrent
|
|
90
|
+
// imports can't clobber each other's tag lists.
|
|
91
|
+
const collected = [];
|
|
92
|
+
const prev = _collector;
|
|
93
|
+
_collector = collected;
|
|
94
|
+
try {
|
|
95
|
+
await import('/' + file + '?t=' + Date.now());
|
|
96
|
+
} finally {
|
|
97
|
+
_collector = prev;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Two HMR paths depending on whether render-cache slot symbols are
|
|
101
|
+
// stable across this re-import:
|
|
102
|
+
//
|
|
103
|
+
// 'stable': server-side symbol stabilization made the new module's
|
|
104
|
+
// anonymous Symbols identical (by reference) to the previous
|
|
105
|
+
// compilation. Live element instances still have valid slot
|
|
106
|
+
// references → imba's renderer will diff and update the existing
|
|
107
|
+
// DOM in place. We just patch class prototypes (already done in
|
|
108
|
+
// the customElements.define hook above) and call imba.commit().
|
|
109
|
+
// No DOM destruction, full inner state preserved.
|
|
110
|
+
//
|
|
111
|
+
// 'shifted': slot count changed (user added/removed elements), so
|
|
112
|
+
// stabilization can't safely reuse symbols. Fall back to the
|
|
113
|
+
// destructive path: snapshot own enumerable properties, wipe
|
|
114
|
+
// ANONYMOUS symbol slots only (preserve global Symbol.for keys
|
|
115
|
+
// that imba's runtime uses for lifecycle), clear innerHTML,
|
|
116
|
+
// restore state, re-render. Loses inner DOM state for instances
|
|
117
|
+
// of the patched tags, but preserves their instance fields.
|
|
118
|
+
if (slots === 'shifted') {
|
|
119
|
+
for (const tag of collected) {
|
|
120
|
+
document.querySelectorAll(tag).forEach(el => {
|
|
121
|
+
const state = {};
|
|
122
|
+
for (const k of Object.keys(el)) state[k] = el[k];
|
|
123
|
+
for (const sym of Object.getOwnPropertySymbols(el)) {
|
|
124
|
+
if (Symbol.keyFor(sym) !== undefined) continue;
|
|
125
|
+
try { delete el[sym]; } catch(_) {}
|
|
126
|
+
}
|
|
127
|
+
el.innerHTML = '';
|
|
128
|
+
Object.assign(el, state);
|
|
129
|
+
try { el.render && el.render(); } catch(_) {}
|
|
130
|
+
});
|
|
105
131
|
}
|
|
132
|
+
}
|
|
106
133
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
134
|
+
if (typeof imba !== 'undefined') imba.commit();
|
|
135
|
+
|
|
136
|
+
// Smart body dedupe: remove only elements that were ADDED during this
|
|
137
|
+
// HMR cycle and whose tag already existed in body before. This catches
|
|
138
|
+
// accidental re-mounts from top-level imba.mount() re-runs, but
|
|
139
|
+
// preserves toasts, multiple modals, and devtools-injected siblings.
|
|
140
|
+
for (const el of [...document.body.children]) {
|
|
141
|
+
if (bodyBefore.has(el)) continue;
|
|
142
|
+
if (tagsBefore.has(el.tagName.toLowerCase())) el.remove();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Reap orphaned imba style blocks. Each compilation that produces a
|
|
146
|
+
// different content hash leaves behind a <style> whose rules target
|
|
147
|
+
// classnames no element in the DOM uses anymore. Walk our tracked
|
|
148
|
+
// styles and drop the unused ones — keeps head clean and removes
|
|
149
|
+
// stale rules that would otherwise still apply to live elements with
|
|
150
|
+
// matching classnames (e.g. a "stuck" old text color).
|
|
151
|
+
_reapStyles();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Style reaper ───────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
// imba_styles.register inserts <style data-id="<hash>"> blocks into <head>.
|
|
157
|
+
// Walk them, sample a few class selectors, and check whether any element
|
|
158
|
+
// in the document still uses one of those classnames. If not, the block
|
|
159
|
+
// is dead (its tag was hot-replaced with a new content hash and the old
|
|
160
|
+
// classnames are gone from the DOM) — remove it.
|
|
161
|
+
function _reapStyles() {
|
|
162
|
+
const styles = document.head.querySelectorAll('style[data-id]');
|
|
163
|
+
for (const style of styles) {
|
|
164
|
+
try {
|
|
165
|
+
const sheet = style.sheet;
|
|
166
|
+
if (!sheet || !sheet.cssRules) continue;
|
|
167
|
+
const probes = [];
|
|
168
|
+
for (const rule of sheet.cssRules) {
|
|
169
|
+
if (probes.length >= 4) break;
|
|
170
|
+
const sel = rule.selectorText;
|
|
171
|
+
if (!sel) continue;
|
|
172
|
+
const m = sel.match(/\.(z[a-z0-9_-]+)/i);
|
|
173
|
+
if (m && !probes.includes(m[1])) probes.push(m[1]);
|
|
174
|
+
}
|
|
175
|
+
if (!probes.length) continue;
|
|
176
|
+
let used = false;
|
|
177
|
+
for (const cls of probes) {
|
|
178
|
+
if (document.querySelector('.' + CSS.escape(cls))) { used = true; break; }
|
|
179
|
+
}
|
|
180
|
+
if (!used) style.remove();
|
|
181
|
+
} catch(_) { /* cross-origin or detached */ }
|
|
182
|
+
}
|
|
119
183
|
}
|
|
120
184
|
|
|
121
185
|
// ── WebSocket connection ───────────────────────────────────────────────────
|