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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/serve.js +116 -52
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimba-cli",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/HeapVoid/bimba.git"
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 _hotTags = []; // tags defined during the current hot import
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
- _hotTags.push(name);
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
- _hotTags = [];
68
-
69
- import('/' + file + '?t=' + Date.now()).then(() => {
70
- const tags = _hotTags;
71
- _hotTags = [];
72
-
73
- // Two HMR paths depending on whether render-cache slot symbols are
74
- // stable across this re-import:
75
- //
76
- // 'stable': server-side symbol stabilization made the new module's
77
- // anonymous Symbols identical (by reference) to the previous
78
- // compilation. Live element instances still have valid slot
79
- // references imba's renderer will diff and update the existing
80
- // DOM in place. We just patch class prototypes (already done in
81
- // the customElements.define hook above) and call imba.commit().
82
- // No DOM destruction, full inner state preserved.
83
- //
84
- // 'shifted': slot count changed (user added/removed elements), so
85
- // stabilization can't safely reuse symbols. Fall back to the
86
- // destructive path: snapshot own enumerable properties, wipe
87
- // ANONYMOUS symbol slots only (preserve global Symbol.for keys
88
- // that imba's runtime uses for lifecycle), clear innerHTML,
89
- // restore state, re-render. Loses inner DOM state for instances
90
- // of the patched tags, but preserves their instance fields.
91
- if (slots === 'shifted') {
92
- for (const tag of tags) {
93
- document.querySelectorAll(tag).forEach(el => {
94
- const state = {};
95
- for (const k of Object.keys(el)) state[k] = el[k];
96
- for (const sym of Object.getOwnPropertySymbols(el)) {
97
- if (Symbol.keyFor(sym) !== undefined) continue;
98
- try { delete el[sym]; } catch(_) {}
99
- }
100
- el.innerHTML = '';
101
- Object.assign(el, state);
102
- try { el.render && el.render(); } catch(_) {}
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
- if (typeof imba !== 'undefined') imba.commit();
108
-
109
- // Dedupe direct body children by tag handles re-importing
110
- // top-level code that calls imba.mount() again, which would
111
- // append a second copy of the root tag.
112
- const seen = new Set();
113
- [...document.body.children].forEach(el => {
114
- const tag = el.tagName.toLowerCase();
115
- if (seen.has(tag)) el.remove();
116
- else seen.add(tag);
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 ───────────────────────────────────────────────────