bimba-cli 0.5.5 → 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 +74 -30
package/package.json
CHANGED
package/serve.js
CHANGED
|
@@ -62,7 +62,7 @@ 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
|
|
|
@@ -70,33 +70,38 @@ const hmrClient = `
|
|
|
70
70
|
const tags = _hotTags;
|
|
71
71
|
_hotTags = [];
|
|
72
72
|
|
|
73
|
-
//
|
|
74
|
-
//
|
|
73
|
+
// Two HMR paths depending on whether render-cache slot symbols are
|
|
74
|
+
// stable across this re-import:
|
|
75
75
|
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
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.
|
|
82
83
|
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
+
}
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
if (typeof imba !== 'undefined') imba.commit();
|
|
@@ -128,7 +133,7 @@ const hmrClient = `
|
|
|
128
133
|
|
|
129
134
|
ws.onmessage = (e) => {
|
|
130
135
|
const msg = JSON.parse(e.data);
|
|
131
|
-
if (msg.type === 'update') _applyUpdate(msg.file);
|
|
136
|
+
if (msg.type === 'update') _applyUpdate(msg.file, msg.slots);
|
|
132
137
|
else if (msg.type === 'reload') location.reload();
|
|
133
138
|
else if (msg.type === 'error') showError(msg.file, msg.errors);
|
|
134
139
|
else if (msg.type === 'clear-error') clearError();
|
|
@@ -177,6 +182,37 @@ const hmrClient = `
|
|
|
177
182
|
|
|
178
183
|
const _compileCache = new Map() // filepath → { mtime, result }
|
|
179
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
|
+
}
|
|
180
216
|
|
|
181
217
|
async function compileFile(filepath) {
|
|
182
218
|
const file = Bun.file(filepath)
|
|
@@ -184,7 +220,7 @@ async function compileFile(filepath) {
|
|
|
184
220
|
const mtime = stat.mtime.getTime()
|
|
185
221
|
|
|
186
222
|
const cached = _compileCache.get(filepath)
|
|
187
|
-
if (cached && cached.mtime === mtime) return { ...cached.result, changeType: 'cached' }
|
|
223
|
+
if (cached && cached.mtime === mtime) return { ...cached.result, changeType: 'cached', slots: cached.slots }
|
|
188
224
|
|
|
189
225
|
const code = await file.text()
|
|
190
226
|
const result = compiler.compile(code, {
|
|
@@ -193,9 +229,17 @@ async function compileFile(filepath) {
|
|
|
193
229
|
sourcemap: 'inline',
|
|
194
230
|
})
|
|
195
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
|
+
|
|
196
240
|
const changeType = _prevJs.get(filepath) === result.js ? 'none' : 'full'
|
|
197
241
|
_prevJs.set(filepath, result.js)
|
|
198
|
-
_compileCache.set(filepath, { mtime, result })
|
|
242
|
+
_compileCache.set(filepath, { mtime, result, slots: result.slots })
|
|
199
243
|
return { ...result, changeType }
|
|
200
244
|
}
|
|
201
245
|
|
|
@@ -351,7 +395,7 @@ export function serve(entrypoint, flags) {
|
|
|
351
395
|
|
|
352
396
|
printStatus(rel, 'ok')
|
|
353
397
|
broadcast({ type: 'clear-error' })
|
|
354
|
-
broadcast({ type: 'update', file: rel })
|
|
398
|
+
broadcast({ type: 'update', file: rel, slots: out.slots || 'shifted' })
|
|
355
399
|
} catch(e) {
|
|
356
400
|
printStatus(rel, 'fail', [{ message: e.message }])
|
|
357
401
|
broadcast({ type: 'error', file: rel, errors: [{ message: e.message, snippet: e.stack || e.message }] })
|