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.
- package/package.json +1 -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.
|
|
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)
|
|
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
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
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)
|
|
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),
|
|
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:
|
|
588
|
-
|
|
589
|
-
// Cascade: re-import
|
|
590
|
-
//
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
|
|
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
|
})
|