bimba-cli 0.5.4 → 0.5.6
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 +82 -21
package/package.json
CHANGED
package/serve.js
CHANGED
|
@@ -62,31 +62,53 @@ const hmrClient = `
|
|
|
62
62
|
|
|
63
63
|
// ── HMR update handler ─────────────────────────────────────────────────────
|
|
64
64
|
|
|
65
|
-
function _applyUpdate(file) {
|
|
65
|
+
function _applyUpdate(file, slots) {
|
|
66
66
|
clearError();
|
|
67
67
|
_hotTags = [];
|
|
68
68
|
|
|
69
69
|
import('/' + file + '?t=' + Date.now()).then(() => {
|
|
70
|
+
const tags = _hotTags;
|
|
70
71
|
_hotTags = [];
|
|
71
72
|
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
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
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
77
107
|
if (typeof imba !== 'undefined') imba.commit();
|
|
78
108
|
|
|
79
|
-
// Dedupe direct body children by tag —
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
// (appends a second root tag to body).
|
|
83
|
-
// 2) Detached popups: a popup uses imba.mount(self) to reparent
|
|
84
|
-
// itself to body, so on commit the parent re-renders, doesn't
|
|
85
|
-
// see the popup among its children, and creates a brand-new
|
|
86
|
-
// instance which gets appended to body. The original (with
|
|
87
|
-
// its state) is first in body.children; the freshly-created
|
|
88
|
-
// duplicate is last — removing later occurrences preserves
|
|
89
|
-
// state.
|
|
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.
|
|
90
112
|
const seen = new Set();
|
|
91
113
|
[...document.body.children].forEach(el => {
|
|
92
114
|
const tag = el.tagName.toLowerCase();
|
|
@@ -111,7 +133,7 @@ const hmrClient = `
|
|
|
111
133
|
|
|
112
134
|
ws.onmessage = (e) => {
|
|
113
135
|
const msg = JSON.parse(e.data);
|
|
114
|
-
if (msg.type === 'update') _applyUpdate(msg.file);
|
|
136
|
+
if (msg.type === 'update') _applyUpdate(msg.file, msg.slots);
|
|
115
137
|
else if (msg.type === 'reload') location.reload();
|
|
116
138
|
else if (msg.type === 'error') showError(msg.file, msg.errors);
|
|
117
139
|
else if (msg.type === 'clear-error') clearError();
|
|
@@ -160,6 +182,37 @@ const hmrClient = `
|
|
|
160
182
|
|
|
161
183
|
const _compileCache = new Map() // filepath → { mtime, result }
|
|
162
184
|
const _prevJs = new Map() // filepath → compiled js — for change detection
|
|
185
|
+
const _prevSlots = new Map() // filepath → previous symbol slot count
|
|
186
|
+
|
|
187
|
+
// Imba compiles tag render-cache slots as anonymous local Symbols at module top
|
|
188
|
+
// level: `var $4 = Symbol(), $11 = Symbol(), ...; let c$0 = Symbol();`. Each
|
|
189
|
+
// re-import of the file creates fresh Symbol objects, so old slot data on live
|
|
190
|
+
// element instances no longer matches the new render's keys, and imba's diff
|
|
191
|
+
// can't reuse cached children — it appends new ones, causing duplication.
|
|
192
|
+
//
|
|
193
|
+
// We rewrite each `<name> = Symbol()` clause so that the Symbol is read from a
|
|
194
|
+
// per-file global cache, keyed by the variable name. On the first compilation
|
|
195
|
+
// the cache is populated; on every subsequent compilation the same Symbol
|
|
196
|
+
// objects are reused, slot keys stay stable, and imba's renderer happily
|
|
197
|
+
// diff-updates existing DOM in place.
|
|
198
|
+
//
|
|
199
|
+
// Caveat: stability is keyed by name. If the user adds/removes elements in the
|
|
200
|
+
// template, slot indices shift and the same name now points to a semantically
|
|
201
|
+
// different slot. We detect this by counting slots — if the count changes vs
|
|
202
|
+
// the previous compilation, we mark the file `slots: 'shifted'` and the client
|
|
203
|
+
// falls back to the destructive wipe-and-render path. Pure CSS/text edits keep
|
|
204
|
+
// counts unchanged → true in-place HMR.
|
|
205
|
+
function stabilizeSymbols(js, filepath) {
|
|
206
|
+
let count = 0
|
|
207
|
+
const out = js.replace(
|
|
208
|
+
/([A-Za-z_$][\w$]*)\s*=\s*Symbol\(\)/g,
|
|
209
|
+
(_m, name) => { count++; return `${name} = (__bsyms__[${JSON.stringify(name)}] ||= Symbol())` }
|
|
210
|
+
)
|
|
211
|
+
if (count === 0) return { js, slotCount: 0 }
|
|
212
|
+
const fileKey = JSON.stringify(filepath)
|
|
213
|
+
const bootstrap = `const __bsyms__ = ((globalThis.__bimba_syms ||= {})[${fileKey}] ||= {});\n`
|
|
214
|
+
return { js: bootstrap + out, slotCount: count }
|
|
215
|
+
}
|
|
163
216
|
|
|
164
217
|
async function compileFile(filepath) {
|
|
165
218
|
const file = Bun.file(filepath)
|
|
@@ -167,7 +220,7 @@ async function compileFile(filepath) {
|
|
|
167
220
|
const mtime = stat.mtime.getTime()
|
|
168
221
|
|
|
169
222
|
const cached = _compileCache.get(filepath)
|
|
170
|
-
if (cached && cached.mtime === mtime) return { ...cached.result, changeType: 'cached' }
|
|
223
|
+
if (cached && cached.mtime === mtime) return { ...cached.result, changeType: 'cached', slots: cached.slots }
|
|
171
224
|
|
|
172
225
|
const code = await file.text()
|
|
173
226
|
const result = compiler.compile(code, {
|
|
@@ -176,9 +229,17 @@ async function compileFile(filepath) {
|
|
|
176
229
|
sourcemap: 'inline',
|
|
177
230
|
})
|
|
178
231
|
|
|
232
|
+
if (!result.errors?.length && result.js) {
|
|
233
|
+
const { js, slotCount } = stabilizeSymbols(result.js, filepath)
|
|
234
|
+
result.js = js
|
|
235
|
+
const prev = _prevSlots.get(filepath)
|
|
236
|
+
result.slots = (prev === undefined || prev === slotCount) ? 'stable' : 'shifted'
|
|
237
|
+
_prevSlots.set(filepath, slotCount)
|
|
238
|
+
}
|
|
239
|
+
|
|
179
240
|
const changeType = _prevJs.get(filepath) === result.js ? 'none' : 'full'
|
|
180
241
|
_prevJs.set(filepath, result.js)
|
|
181
|
-
_compileCache.set(filepath, { mtime, result })
|
|
242
|
+
_compileCache.set(filepath, { mtime, result, slots: result.slots })
|
|
182
243
|
return { ...result, changeType }
|
|
183
244
|
}
|
|
184
245
|
|
|
@@ -334,7 +395,7 @@ export function serve(entrypoint, flags) {
|
|
|
334
395
|
|
|
335
396
|
printStatus(rel, 'ok')
|
|
336
397
|
broadcast({ type: 'clear-error' })
|
|
337
|
-
broadcast({ type: 'update', file: rel })
|
|
398
|
+
broadcast({ type: 'update', file: rel, slots: out.slots || 'shifted' })
|
|
338
399
|
} catch(e) {
|
|
339
400
|
printStatus(rel, 'fail', [{ message: e.message }])
|
|
340
401
|
broadcast({ type: 'error', file: rel, errors: [{ message: e.message, snippet: e.stack || e.message }] })
|