//go:build dev package bundler import "net/http" // hmrUpdate names one boundary module to re-import at a given version. type hmrUpdate struct { Path string `json:"path"` Timestamp int64 `json:"timestamp"` } // hmrError describes a compile/transform failure surfaced to the browser as a // full-screen error overlay (Vite-style). Message is the full formatted error // text (esbuild carries a code frame; the Solid compiler a plain message). type hmrError struct { Message string `json:"message"` File string `json:"file,omitempty"` } // hmrMessage is the WebSocket payload pushed to the browser. type hmrMessage struct { Type string `json:"type"` // "update" | "full-reload" | "css-update" | "error" Updates []hmrUpdate `json:"updates,omitempty"` Path string `json:"path,omitempty"` // css-update: the stylesheet path Err *hmrError `json:"err,omitempty"` // error: the compile failure to display } // serveClient serves the HMR client runtime as an ES module. func (d *devServer) serveClient(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/javascript; charset=utf-8") w.Header().Set("Cache-Control", "no-cache") w.Write([]byte(hmrClientJS)) } // hmrClientJS is the browser runtime: it owns the WebSocket, exposes // createHotContext (the import.meta.hot the transformed modules bind), and // applies updates. The accept/dispose/data protocol mirrors Vite's so // solid-refresh's `esm` path (hot.data + hot.accept(cb) + hot.invalidate) works // unchanged — a changed boundary is re-imported, and the PREVIOUS instance's // accept callback runs with the new module namespace, patching the live registry. // // It also exports showErrorOverlay/clearErrorOverlay and renders a Vite-style // full-screen compile-error overlay. A module that fails to transform is served // as a tiny stub that imports showErrorOverlay and calls it (see errorModule in // hmr_server.go), so the overlay pops the instant a broken module is imported — // on first load or on a hot re-import. The overlay auto-clears once a hot-update // batch completes without any module surfacing an error (see applyUpdates). const hmrClientJS = ` // --- module-level HMR state, keyed by base module URL -------------------------- const hotModulesMap = new Map(); // id -> { id, callbacks: [{fn}] } const dataMap = new Map(); // id -> persistent data object (survives reloads) const disposeMap = new Map(); // id -> dispose callback const declined = new Set(); // ids that opted out of HMR export function createHotContext(id) { if (!dataMap.has(id)) dataMap.set(id, {}); const existing = hotModulesMap.get(id); // A fresh instance of this module is registering; clear its accept callbacks // (applyUpdate has already snapshotted the previous instance's). if (existing) existing.callbacks = []; function pushAccept(fn) { let mod = hotModulesMap.get(id); if (!mod) { mod = { id, callbacks: [] }; hotModulesMap.set(id, mod); } mod.callbacks.push({ fn }); } return { get data() { return dataMap.get(id); }, accept(deps, cb) { // accept() | accept(fn) | accept(deps, fn) — self-accept in every form we use. if (typeof deps === 'function' || deps == null) pushAccept(deps); else pushAccept(cb); }, dispose(cb) { disposeMap.set(id, cb); }, prune(cb) { disposeMap.set(id, cb); }, invalidate() { fullReload(); }, decline() { declined.add(id); }, on() {}, off() {}, send() {}, }; } // --- recompile indicator ------------------------------------------------------- // A small, non-blocking badge so a recompile doesn't look like a frozen page: // shown the moment an update arrives and hidden once the new module is imported // and applied. The // await below yields the event loop, so the spinner paints and animates while the // server compiles. let hmrBusy = 0; let hmrEl = null; function hmrIndicator() { if (hmrEl || typeof document === 'undefined') return hmrEl; const head = document.head || document.documentElement; const style = document.createElement('style'); style.textContent = '@keyframes hmr-spin{to{transform:rotate(360deg)}}'; head.appendChild(style); hmrEl = document.createElement('div'); hmrEl.setAttribute('style', 'position:fixed;bottom:14px;right:14px;z-index:2147483647;display:none;' + 'align-items:center;gap:8px;padding:7px 12px;border-radius:9px;' + 'font:600 12px/1 ui-monospace,SFMono-Regular,Menlo,monospace;color:#e5e7eb;' + 'background:rgba(17,24,39,.92);box-shadow:0 6px 18px rgba(0,0,0,.35);' + 'pointer-events:none;user-select:none'); hmrEl.innerHTML = ''; (document.body || document.documentElement).appendChild(hmrEl); return hmrEl; } function hmrShow(label) { const el = hmrIndicator(); if (!el) return; const l = el.querySelector('[data-hmr-label]'); if (l) l.textContent = label || 'recompiling…'; el.style.display = 'flex'; } function hmrBusyStart() { hmrBusy++; hmrShow('recompiling…'); } function hmrBusyEnd() { hmrBusy = Math.max(0, hmrBusy - 1); if (hmrBusy === 0 && hmrEl) hmrEl.style.display = 'none'; } // --- compile-error overlay ----------------------------------------------------- // A Vite-style full-screen overlay for compile/transform failures. errorEpoch is // bumped every time an error surfaces; applyUpdates snapshots it around a hot // batch and clears the overlay only if the batch introduced no new error, so a // fixed file dismisses the overlay automatically. The overlay lives in a shadow // root so the app's stylesheet (Tailwind reset et al.) can't restyle it. let overlayEl = null; let errorEpoch = 0; const HMR_OVERLAY_ID = '__hmr-error-overlay'; function escapeHTML(s) { return String(s).replace(/[&<>]/g, function (c) { return c === '&' ? '&' : c === '<' ? '<' : '>'; }); } function overlayHTML(message, file) { const css = ':host{all:initial}' + '.backdrop{position:fixed;inset:0;z-index:2147483647;background:rgba(0,0,0,.66);' + 'display:flex;align-items:flex-start;justify-content:center;overflow:auto;padding:6vh 20px;' + 'font:14px/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}' + '.panel{width:100%;max-width:min(1000px,92vw);margin:auto 0;background:#1b1b1f;color:#e6e6e6;' + 'border:1px solid #ff5555;border-radius:10px;box-shadow:0 20px 60px rgba(0,0,0,.5);overflow:hidden}' + '.head{display:flex;align-items:center;gap:10px;padding:12px 14px;background:#2a1416;' + 'border-bottom:1px solid rgba(255,85,85,.35)}' + '.badge{color:#ff6b6b;font-weight:700;letter-spacing:.03em;text-transform:uppercase;font-size:12px}' + '.file{color:#9aa0a6;font-size:12.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' + '.close{margin-left:auto;background:transparent;border:0;color:#9aa0a6;cursor:pointer;' + 'font-size:16px;line-height:1;padding:4px 7px;border-radius:6px}' + '.close:hover{color:#fff;background:rgba(255,255,255,.08)}' + '.body{margin:0;padding:16px;white-space:pre-wrap;word-break:break-word;color:#ffb4b4;' + 'font-size:13px;max-height:62vh;overflow:auto}' + '.hint{padding:10px 14px;border-top:1px solid rgba(255,255,255,.06);color:#7c828a;font-size:12px}'; return '' + '
' + escapeHTML(message) + '' + '