bimba-cli 0.5.15 → 0.5.16

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 -2
  2. package/serve.js +83 -122
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bimba-cli",
3
- "version": "0.5.15",
3
+ "version": "0.5.16",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/HeapVoid/bimba.git"
@@ -17,7 +17,6 @@
17
17
  "license": "MIT",
18
18
  "type": "module",
19
19
  "devDependencies": {
20
- "bimba-cli": "/Users/fedor/Projects/modules/bimba/bimba-cli-0.5.14.tgz",
21
20
  "imba": "latest"
22
21
  }
23
22
  }
package/serve.js CHANGED
@@ -25,6 +25,8 @@ const hmrClient = `
25
25
  //
26
26
  const _origDefine = customElements.define.bind(customElements);
27
27
  const _classes = new Map(); // tagName → first-registered constructor
28
+ const _newClasses = new Map(); // tagName → latest class from HMR import
29
+ const _oldNs = new Map(); // tagName → previous _ns_ (saved before _patchClass wipes it)
28
30
  let _collector = null; // when set, captures tag names defined during one HMR import
29
31
 
30
32
  customElements.define = function(name, cls, opts) {
@@ -34,8 +36,13 @@ const hmrClient = `
34
36
  _origDefine(name, cls, opts);
35
37
  _classes.set(name, cls);
36
38
  } else {
39
+ _newClasses.set(name, cls);
37
40
  const target = _classes.get(name);
38
- if (target) _patchClass(target, cls);
41
+ if (target) {
42
+ // Save old _ns_ before _patchClass overwrites prototype descriptors
43
+ if (target.prototype._ns_) _oldNs.set(name, target.prototype._ns_);
44
+ _patchClass(target, cls);
45
+ }
39
46
  }
40
47
  };
41
48
 
@@ -92,17 +99,10 @@ const hmrClient = `
92
99
  async function _doUpdate(file, slots) {
93
100
  clearError();
94
101
 
95
- // Snapshot direct body children BEFORE importing the new module, so we
96
- // know which elements pre-existed. After commit we only dedupe NEW
97
- // elements whose tag also existed before — this preserves legitimate
98
- // multi-instance roots like toasts and parallel popups, while still
99
- // catching accidental re-mounts from re-running top-level code.
100
102
  const bodyBefore = new Set(document.body.children);
101
103
  const tagsBefore = new Set();
102
104
  for (const el of bodyBefore) tagsBefore.add(el.tagName.toLowerCase());
103
105
 
104
- // Use a local collector instead of a shared variable so concurrent
105
- // imports can't clobber each other's tag lists.
106
106
  const collected = [];
107
107
  const prev = _collector;
108
108
  _collector = collected;
@@ -112,119 +112,77 @@ const hmrClient = `
112
112
  _collector = prev;
113
113
  }
114
114
 
115
- // Two HMR paths depending on whether render-cache slot symbols are
116
- // stable across this re-import:
117
- //
118
- // 'stable': server-side symbol stabilization made the new module's
119
- // anonymous Symbols identical (by reference) to the previous
120
- // compilation. Live element instances still have valid slot
121
- // references → imba's renderer will diff and update the existing
122
- // DOM in place. We just patch class prototypes (already done in
123
- // the customElements.define hook above) and call imba.commit().
124
- // No DOM destruction, full inner state preserved.
125
- //
126
- // 'shifted': slot count changed (user added/removed elements), so
127
- // stabilization can't safely reuse symbols. Fall back to the
128
- // destructive path: snapshot own enumerable properties, wipe
129
- // ANONYMOUS symbol slots only (preserve global Symbol.for keys
130
- // that imba's runtime uses for lifecycle), clear innerHTML,
131
- // restore state, re-render. Loses inner DOM state for instances
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();
115
+ // Sync _ns_ (CSS namespace) from the new classes. imba_defineTag sets
116
+ // _ns_ on NewClass.prototype AFTER register$ calls customElements.define,
117
+ // so _patchClass missed it. Now that import is done, all _ns_ values are set.
118
+ // Save old→new mapping for className patching below.
119
+ const _nsPatches = []; // [{ oldParts, newParts }]
139
120
  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
-
144
- if (slots === 'shifted') {
145
- for (const tag of collected) {
146
- document.querySelectorAll(tag).forEach(el => {
147
- const state = {};
148
- for (const k of Object.keys(el)) state[k] = el[k];
149
- _disconnectDescendants(el);
150
- for (const sym of Object.getOwnPropertySymbols(el)) {
151
- if (Symbol.keyFor(sym) !== undefined) continue;
152
- try { delete el[sym]; } catch(_) {}
153
- }
154
- el.innerHTML = '';
155
- Object.assign(el, state);
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(_) {}
121
+ const newCls = _newClasses.get(tag);
122
+ const oldCls = _classes.get(tag);
123
+ const newNs = newCls?.prototype._ns_;
124
+ const oldNs = _oldNs.get(tag);
125
+ if (oldNs && newNs && oldNs !== newNs) {
126
+ oldCls.prototype._ns_ = newNs;
127
+ // _ns_ uses '_' separator (z12kthg6_bc), className uses '-' (z12kthg6-bc)
128
+ _nsPatches.push({
129
+ oldParts: oldNs.trim().split(/\\s+/).map(s => s.replace(/_/g, '-')),
130
+ newParts: newNs.trim().split(/\\s+/).map(s => s.replace(/_/g, '-')),
164
131
  });
132
+ } else if (newNs && oldCls && oldCls.prototype._ns_ !== newNs) {
133
+ oldCls.prototype._ns_ = newNs;
165
134
  }
135
+ _oldNs.delete(tag);
136
+ }
137
+
138
+ // Destructive HMR: wipe inner DOM and re-render each collected tag
139
+ for (const tag of collected) {
140
+ const els = document.querySelectorAll(tag);
141
+ els.forEach(el => {
142
+ const state = {};
143
+ for (const k of Object.keys(el)) state[k] = el[k];
144
+ _disconnectDescendants(el);
145
+ for (const sym of Object.getOwnPropertySymbols(el)) {
146
+ if (Symbol.keyFor(sym) !== undefined) continue;
147
+ try { delete el[sym]; } catch(_) {}
148
+ }
149
+ el.innerHTML = '';
150
+ Object.assign(el, state);
151
+ try { el.render && el.render(); } catch(e) { console.error('[bimba] render error:', e); }
152
+ try { el.connectedCallback && el.connectedCallback(); } catch(_) {}
153
+ try { el.mount && el.mount(); } catch(_) {}
154
+ });
166
155
  }
167
156
 
168
157
  if (typeof imba !== 'undefined') imba.commit();
169
158
 
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;
159
+ // Patch className on ALL custom elements: replace old CSS namespace
160
+ // hashes with new ones. Must be global because subclass elements
161
+ // (e.g. panel-agent < basic-panel) inherit the parent's _ns_ hash
162
+ // but querySelectorAll('basic-panel') won't find them.
163
+ if (_nsPatches.length) {
164
+ document.querySelectorAll('*').forEach(el => {
165
+ if (!el.tagName.includes('-')) return;
166
+ let cn = el.className;
167
+ if (!cn) return;
168
+ let changed = false;
169
+ for (const { oldParts, newParts } of _nsPatches) {
170
+ for (let i = 0; i < Math.min(oldParts.length, newParts.length); i++) {
171
+ if (cn.includes(oldParts[i])) {
172
+ cn = cn.split(oldParts[i]).join(newParts[i]);
173
+ changed = true;
174
+ }
175
+ }
177
176
  }
178
- }
177
+ if (changed) el.className = cn;
178
+ });
179
179
  }
180
180
 
181
- // Smart body dedupe: remove only elements that were ADDED during this
182
- // HMR cycle and whose tag already existed in body before. This catches
183
- // accidental re-mounts from top-level imba.mount() re-runs, but
184
- // preserves toasts, multiple modals, and devtools-injected siblings.
181
+ // Smart body dedupe: remove duplicate top-level elements created by re-import
185
182
  for (const el of [...document.body.children]) {
186
183
  if (bodyBefore.has(el)) continue;
187
184
  if (tagsBefore.has(el.tagName.toLowerCase())) el.remove();
188
185
  }
189
-
190
- // Reap orphaned imba style blocks. Each compilation that produces a
191
- // different content hash leaves behind a <style> whose rules target
192
- // classnames no element in the DOM uses anymore. Walk our tracked
193
- // styles and drop the unused ones — keeps head clean and removes
194
- // stale rules that would otherwise still apply to live elements with
195
- // matching classnames (e.g. a "stuck" old text color).
196
- _reapStyles();
197
- }
198
-
199
- // ── Style reaper ───────────────────────────────────────────────────────────
200
-
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.
206
- function _reapStyles() {
207
- const styles = document.head.querySelectorAll('style[data-id]');
208
- for (const style of styles) {
209
- try {
210
- const sheet = style.sheet;
211
- if (!sheet || !sheet.cssRules) continue;
212
- const probes = [];
213
- for (const rule of sheet.cssRules) {
214
- if (probes.length >= 4) break;
215
- const sel = rule.selectorText;
216
- if (!sel) continue;
217
- const m = sel.match(/\.(z[a-z0-9_-]+)/i);
218
- if (m && !probes.includes(m[1])) probes.push(m[1]);
219
- }
220
- if (!probes.length) continue;
221
- let used = false;
222
- for (const cls of probes) {
223
- if (document.querySelector('.' + CSS.escape(cls))) { used = true; break; }
224
- }
225
- if (!used) style.remove();
226
- } catch(_) { /* cross-origin or detached */ }
227
- }
228
186
  }
229
187
 
230
188
  // ── WebSocket connection ───────────────────────────────────────────────────
@@ -235,7 +193,6 @@ const hmrClient = `
235
193
  const ws = new WebSocket('ws://' + location.host + '/__hmr__');
236
194
 
237
195
  ws.onopen = () => {
238
- // If we reconnect after a disconnect, reload to get fresh state
239
196
  if (_connected) location.reload();
240
197
  else _connected = true;
241
198
  };
@@ -343,14 +300,18 @@ function updateImportGraph(fromAbs, newDeps) {
343
300
  _imports.set(fromAbs, newDeps)
344
301
  }
345
302
 
346
- function transitiveImporters(absFile) {
303
+ function transitiveImporters(absFile, skip) {
347
304
  const out = new Set()
348
305
  const stack = [absFile]
349
306
  while (stack.length) {
350
307
  const cur = stack.pop()
351
308
  const ups = _importers.get(cur)
352
309
  if (!ups) continue
353
- for (const u of ups) if (!out.has(u)) { out.add(u); stack.push(u) }
310
+ for (const u of ups) {
311
+ if (out.has(u) || (skip && skip.has(u))) continue
312
+ out.add(u)
313
+ stack.push(u)
314
+ }
354
315
  }
355
316
  return out
356
317
  }
@@ -487,6 +448,7 @@ export function serve(entrypoint, flags) {
487
448
  const htmlDir = path.dirname(htmlPath)
488
449
  const srcDir = path.dirname(entrypoint)
489
450
  const sockets = new Set()
451
+ const entryAbs = path.resolve(entrypoint)
490
452
  let importMapTag = null
491
453
 
492
454
  // ── Status line (prints current compile result, fades out on success) ──────
@@ -561,7 +523,7 @@ export function serve(entrypoint, flags) {
561
523
  watch(srcDir, { recursive: true }, async (_event, filename) => {
562
524
  if (!filename || !filename.endsWith('.imba')) return
563
525
  if (_debounce.has(filename)) return
564
- _debounce.set(filename, setTimeout(() => _debounce.delete(filename), 50))
526
+ _debounce.set(filename, setTimeout(() => _debounce.delete(filename), 150))
565
527
 
566
528
  const filepath = path.join(srcDir, filename)
567
529
  const rel = path.join(path.relative('.', srcDir), filename).replaceAll('\\', '/')
@@ -569,6 +531,7 @@ export function serve(entrypoint, flags) {
569
531
  try {
570
532
  const out = await compileFile(filepath)
571
533
 
534
+
572
535
  if (out.errors?.length) {
573
536
  printStatus(rel, 'fail', out.errors)
574
537
  broadcast({ type: 'error', file: rel, errors: out.errors.map(e => ({
@@ -584,15 +547,13 @@ export function serve(entrypoint, flags) {
584
547
 
585
548
  printStatus(rel, 'ok')
586
549
  broadcast({ type: 'clear-error' })
587
- broadcast({ type: 'update', file: rel, slots: out.slots || 'shifted' })
588
-
589
- // Cascade: re-import every module transitively importing this file.
590
- // They don't need recompilation (their source didn't change), but
591
- // their captured references to the changed module are stale, so we
592
- // tell the client to re-import them. The client's HMR queue
593
- // processes these in order; tag classes get re-patched, plain
594
- // utility modules get fresh top-level state.
595
- const ups = transitiveImporters(path.resolve(filepath))
550
+ broadcast({ type: 'update', file: rel, slots: 'shifted' })
551
+
552
+ // Cascade: re-import modules that transitively import this file.
553
+ // Skip the entry point re-importing it re-runs imba.mount and
554
+ // recreates global services, and traversing through it would
555
+ // cascade to the entire project.
556
+ const ups = transitiveImporters(path.resolve(filepath), new Set([entryAbs]))
596
557
  for (const upAbs of ups) {
597
558
  const upRel = path.relative('.', upAbs).replaceAll('\\', '/')
598
559
  const cached = _compileCache.get(upAbs)
@@ -674,8 +635,8 @@ export function serve(entrypoint, flags) {
674
635
  },
675
636
 
676
637
  websocket: {
677
- open: ws => sockets.add(ws),
678
- close: ws => sockets.delete(ws),
638
+ open: ws => { sockets.add(ws) },
639
+ close: ws => { sockets.delete(ws) },
679
640
  message: () => {},
680
641
  },
681
642
  })