@bakery-framework/core 1.0.0
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/LICENSE +19 -0
- package/README.md +89 -0
- package/package.json +69 -0
- package/src/cache/index.ts +8 -0
- package/src/cache/lru.ts +41 -0
- package/src/cache/shared-db.ts +51 -0
- package/src/cache/string.ts +150 -0
- package/src/cache/tiered.ts +493 -0
- package/src/client/globals.d.ts +74 -0
- package/src/client/livereload.ts +437 -0
- package/src/client/utils.ts +315 -0
- package/src/compiler/compiler.ts +263 -0
- package/src/compiler/dev-service.ts +660 -0
- package/src/compiler/index.ts +2 -0
- package/src/compiler/prompt-tracker.ts +36 -0
- package/src/compiler/tsconfig-sync.ts +71 -0
- package/src/core/bakery.ts +96 -0
- package/src/core/cache-version.ts +119 -0
- package/src/core/config.ts +296 -0
- package/src/core/context.ts +121 -0
- package/src/core/index.ts +61 -0
- package/src/core/init.ts +90 -0
- package/src/core/jsx.ts +152 -0
- package/src/core/paths.ts +24 -0
- package/src/core/plugins.ts +120 -0
- package/src/core/port.ts +73 -0
- package/src/global.d.ts +374 -0
- package/src/handlers/assets/google-font.ts +225 -0
- package/src/handlers/assets/image.ts +136 -0
- package/src/handlers/assets/nm.ts +73 -0
- package/src/handlers/assets/public.ts +17 -0
- package/src/handlers/assets/static.ts +86 -0
- package/src/handlers/assets/ts.ts +61 -0
- package/src/handlers/assets/tsx.ts +106 -0
- package/src/handlers/assets/virtual-asset.ts +104 -0
- package/src/handlers/core/$base.ts +256 -0
- package/src/handlers/core/$dynamic.ts +285 -0
- package/src/handlers/core/$error.ts +301 -0
- package/src/handlers/core/$middleware.ts +71 -0
- package/src/handlers/core/$mounts.ts +84 -0
- package/src/handlers/core/$registry.ts +153 -0
- package/src/handlers/core/$routing.ts +205 -0
- package/src/handlers/core/$static.ts +100 -0
- package/src/handlers/core/$websocket.ts +52 -0
- package/src/handlers/index.ts +21 -0
- package/src/handlers/routes/api.ts +95 -0
- package/src/handlers/routes/html.ts +95 -0
- package/src/handlers/routes/livereload.ts +54 -0
- package/src/handlers/routes/proxy.ts +74 -0
- package/src/logger/clients.ts +12 -0
- package/src/logger/index.ts +3 -0
- package/src/logger/logger.ts +375 -0
- package/src/logger/serve-log.ts +206 -0
- package/src/plugins/index.ts +15 -0
- package/src/plugins/routes.ts +110 -0
- package/src/plugins/types.ts +19 -0
- package/src/router.ts +351 -0
- package/src/session.ts +556 -0
- package/src/shared.d.ts +63 -0
- package/src/startup.ts +154 -0
- package/src/types.d.ts +111 -0
- package/src/utils/common/case.ts +11 -0
- package/src/utils/common/index.ts +5 -0
- package/src/utils/common/json.ts +35 -0
- package/src/utils/common/match.ts +6 -0
- package/src/utils/common/misc.ts +53 -0
- package/src/utils/common/try.ts +6 -0
- package/src/utils/constants.ts +153 -0
- package/src/utils/fs.ts +621 -0
- package/src/utils/http/body.ts +65 -0
- package/src/utils/http/csrf.ts +111 -0
- package/src/utils/http/dom.ts +238 -0
- package/src/utils/http/escape.ts +8 -0
- package/src/utils/http/etag.ts +318 -0
- package/src/utils/http/html.ts +525 -0
- package/src/utils/http/index.ts +8 -0
- package/src/utils/http/ip.ts +32 -0
- package/src/utils/http/response.ts +129 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/isomorphic/case.ts +52 -0
- package/src/utils/isomorphic/escape.ts +43 -0
- package/src/utils/isomorphic/index.ts +15 -0
- package/src/utils/isomorphic/is.ts +36 -0
- package/src/utils/isomorphic/match.ts +50 -0
- package/src/utils/isomorphic/math.ts +11 -0
- package/src/utils/isomorphic/misc.ts +22 -0
- package/src/utils/isomorphic/stringify.ts +42 -0
- package/src/utils/isomorphic/try.ts +94 -0
- package/src/utils/jsonc.ts +10 -0
- package/src/utils/shared-pool.ts +193 -0
- package/tsconfig.app.json +34 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { safeStringify } from '../utils/isomorphic/stringify'
|
|
2
|
+
|
|
3
|
+
let needsReload = false
|
|
4
|
+
let isDead = false
|
|
5
|
+
let consoleHooked = false
|
|
6
|
+
let reconnectAttempts = 0
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Buffered log frames for a socket that isn't open yet. Bounded: a tab left
|
|
10
|
+
* open against a stopped dev server would otherwise accumulate every log line
|
|
11
|
+
* in memory and flood the server on reconnect.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_LOG_QUEUE = 500
|
|
14
|
+
const logQueue: string[] = []
|
|
15
|
+
|
|
16
|
+
function queueLog(msg: string) {
|
|
17
|
+
logQueue.push(msg)
|
|
18
|
+
if (logQueue.length > MAX_LOG_QUEUE) logQueue.shift()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const OVERLAY_ID = 'bakery-livereload-overlay'
|
|
22
|
+
|
|
23
|
+
function hideOverlay() {
|
|
24
|
+
document.getElementById(OVERLAY_ID)?.remove()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Full-viewport dev overlay for server-pushed errors and a dead dev server.
|
|
29
|
+
* Built strictly with createElement/textContent — the title and body arrive
|
|
30
|
+
* over the wire and may contain markup-shaped text (stack traces quoting
|
|
31
|
+
* generics, user file names); nothing here may pass through innerHTML.
|
|
32
|
+
*/
|
|
33
|
+
function showOverlay(title: string, body: string) {
|
|
34
|
+
hideOverlay()
|
|
35
|
+
|
|
36
|
+
const overlay = document.createElement('div')
|
|
37
|
+
overlay.id = OVERLAY_ID
|
|
38
|
+
overlay.style.cssText =
|
|
39
|
+
'position:fixed;inset:0;z-index:2147483647;background:rgba(12,12,14,0.88);' +
|
|
40
|
+
'color:#f5f5f5;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;' +
|
|
41
|
+
'padding:32px;overflow:auto;box-sizing:border-box;cursor:pointer'
|
|
42
|
+
|
|
43
|
+
const titleEl = document.createElement('div')
|
|
44
|
+
titleEl.textContent = title
|
|
45
|
+
titleEl.style.cssText =
|
|
46
|
+
'color:#ff6b6b;font-size:16px;font-weight:700;margin-bottom:16px'
|
|
47
|
+
|
|
48
|
+
const bodyEl = document.createElement('pre')
|
|
49
|
+
bodyEl.textContent = body
|
|
50
|
+
bodyEl.style.cssText =
|
|
51
|
+
'white-space:pre-wrap;font-size:13px;line-height:1.5;margin:0;font-family:inherit'
|
|
52
|
+
|
|
53
|
+
const hint = document.createElement('div')
|
|
54
|
+
hint.textContent = 'click anywhere or press Esc to dismiss'
|
|
55
|
+
hint.style.cssText = 'margin-top:24px;font-size:11px;opacity:0.6'
|
|
56
|
+
|
|
57
|
+
overlay.append(titleEl, bodyEl, hint)
|
|
58
|
+
overlay.addEventListener('click', hideOverlay)
|
|
59
|
+
document.body.appendChild(overlay)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
document.addEventListener('keydown', e => {
|
|
63
|
+
if (e.key === 'Escape') hideOverlay()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
function getHtmlDifference(htmlA: string, htmlB: string): number {
|
|
67
|
+
const getBigrams = (str: string) => {
|
|
68
|
+
const s = str.replace(/\s+/g, '')
|
|
69
|
+
const bigrams = new Set<string>()
|
|
70
|
+
for (let i = 0; i < s.length - 1; i++) {
|
|
71
|
+
bigrams.add(s.substring(i, i + 2))
|
|
72
|
+
}
|
|
73
|
+
return bigrams
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const setA = getBigrams(htmlA)
|
|
77
|
+
const setB = getBigrams(htmlB)
|
|
78
|
+
|
|
79
|
+
const intersection = setA.intersection(setB).size
|
|
80
|
+
|
|
81
|
+
const similarity = (2.0 * intersection) / (setA.size + setB.size) || 0
|
|
82
|
+
return (1 - similarity) * 100
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function replaceNode(current: Node, incoming: Node) {
|
|
86
|
+
if (current.parentNode) {
|
|
87
|
+
current.parentNode.replaceChild(
|
|
88
|
+
document.importNode(incoming, true),
|
|
89
|
+
current,
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function updateTextOrCommentNode(current: Node, incoming: Node) {
|
|
95
|
+
if (current.nodeValue !== incoming.nodeValue) {
|
|
96
|
+
current.nodeValue = incoming.nodeValue
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function patchAttributes(curEl: Element, incEl: Element) {
|
|
101
|
+
for (const attr of Array.from(incEl.attributes)) {
|
|
102
|
+
if (curEl.getAttribute(attr.name) !== attr.value) {
|
|
103
|
+
curEl.setAttribute(attr.name, attr.value)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
for (const attr of Array.from(curEl.attributes)) {
|
|
107
|
+
if (!incEl.hasAttribute(attr.name)) {
|
|
108
|
+
curEl.removeAttribute(attr.name)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function patchInputFields(curEl: Element, incEl: Element) {
|
|
114
|
+
if (curEl instanceof HTMLInputElement && incEl instanceof HTMLInputElement) {
|
|
115
|
+
if (curEl.value !== incEl.value) {
|
|
116
|
+
curEl.value = incEl.value
|
|
117
|
+
}
|
|
118
|
+
if (curEl.checked !== incEl.checked) {
|
|
119
|
+
curEl.checked = incEl.checked
|
|
120
|
+
}
|
|
121
|
+
} else if (
|
|
122
|
+
curEl instanceof HTMLTextAreaElement &&
|
|
123
|
+
incEl instanceof HTMLTextAreaElement
|
|
124
|
+
) {
|
|
125
|
+
if (curEl.value !== incEl.value) {
|
|
126
|
+
curEl.value = incEl.value
|
|
127
|
+
}
|
|
128
|
+
} else if (
|
|
129
|
+
curEl instanceof HTMLSelectElement &&
|
|
130
|
+
incEl instanceof HTMLSelectElement
|
|
131
|
+
) {
|
|
132
|
+
if (curEl.value !== incEl.value) {
|
|
133
|
+
curEl.value = incEl.value
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function patchChildNodes(curEl: Element, incEl: Element) {
|
|
139
|
+
const curChildren = Array.from(curEl.childNodes)
|
|
140
|
+
const incChildren = Array.from(incEl.childNodes)
|
|
141
|
+
const minLen = Math.min(curChildren.length, incChildren.length)
|
|
142
|
+
|
|
143
|
+
for (let i = 0; i < minLen; i++) {
|
|
144
|
+
const curChild = curChildren[i]
|
|
145
|
+
const incChild = incChildren[i]
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
curChild.nodeType === incChild.nodeType &&
|
|
149
|
+
(curChild.nodeType !== Node.ELEMENT_NODE ||
|
|
150
|
+
(curChild as Element).tagName === (incChild as Element).tagName)
|
|
151
|
+
) {
|
|
152
|
+
patchDOM(curChild, incChild)
|
|
153
|
+
} else {
|
|
154
|
+
if (curChild.parentNode === curEl) {
|
|
155
|
+
curEl.replaceChild(document.importNode(incChild, true), curChild)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (let i = minLen; i < curChildren.length; i++) {
|
|
161
|
+
const child = curChildren[i]
|
|
162
|
+
if (child.parentNode === curEl) {
|
|
163
|
+
curEl.removeChild(child)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (let i = minLen; i < incChildren.length; i++) {
|
|
168
|
+
curEl.appendChild(document.importNode(incChildren[i], true))
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function patchElementNode(curEl: Element, incEl: Element) {
|
|
173
|
+
if (curEl.tagName !== incEl.tagName) {
|
|
174
|
+
replaceNode(curEl, incEl)
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
patchAttributes(curEl, incEl)
|
|
179
|
+
patchInputFields(curEl, incEl)
|
|
180
|
+
patchChildNodes(curEl, incEl)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function patchDOM(current: Node, incoming: Node) {
|
|
184
|
+
if (current.nodeType !== incoming.nodeType) {
|
|
185
|
+
replaceNode(current, incoming)
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (
|
|
190
|
+
current.nodeType === Node.TEXT_NODE ||
|
|
191
|
+
current.nodeType === Node.COMMENT_NODE
|
|
192
|
+
) {
|
|
193
|
+
updateTextOrCommentNode(current, incoming)
|
|
194
|
+
return
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (current.nodeType === Node.ELEMENT_NODE) {
|
|
198
|
+
patchElementNode(current as Element, incoming as Element)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function connect() {
|
|
203
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
204
|
+
const ws = new WebSocket(`${protocol}//${location.host}/_livereload`)
|
|
205
|
+
|
|
206
|
+
const sendLog = (level: string, args: any[]) => {
|
|
207
|
+
const payload = Array.from(args)
|
|
208
|
+
.map(a => safeStringify(a))
|
|
209
|
+
.join(' ')
|
|
210
|
+
|
|
211
|
+
const msg = JSON.stringify({
|
|
212
|
+
type: 'client_log',
|
|
213
|
+
level,
|
|
214
|
+
payload,
|
|
215
|
+
ip: '',
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
ws.readyState === WebSocket.OPEN ? ws.send(msg) : queueLog(msg)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// connect() runs again on every reconnect, so this must only happen once.
|
|
222
|
+
// Re-wrapping meant console.log nested one level deeper per reconnect and
|
|
223
|
+
// emitted a duplicate frame each time, plus a new listener per cycle.
|
|
224
|
+
if (!consoleHooked) {
|
|
225
|
+
consoleHooked = true
|
|
226
|
+
|
|
227
|
+
const ogLog = console.log,
|
|
228
|
+
ogWarn = console.warn,
|
|
229
|
+
ogErr = console.error
|
|
230
|
+
console.log = (...args) => {
|
|
231
|
+
ogLog(...args)
|
|
232
|
+
sendLog('info', args)
|
|
233
|
+
}
|
|
234
|
+
console.warn = (...args) => {
|
|
235
|
+
ogWarn(...args)
|
|
236
|
+
sendLog('warn', args)
|
|
237
|
+
}
|
|
238
|
+
console.error = (...args) => {
|
|
239
|
+
ogErr(...args)
|
|
240
|
+
sendLog('error', args)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
window.onerror = (m, s, l, c) =>
|
|
244
|
+
sendLog('error', [`${m} at ${s}:${l}:${c}`])
|
|
245
|
+
window.addEventListener('unhandledrejection', e =>
|
|
246
|
+
sendLog('error', [`Unhandled Promise: ${e.reason}`]),
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const isSameFile = (fileA: string, fileB: string): boolean => {
|
|
251
|
+
const norm = (f: string) =>
|
|
252
|
+
f
|
|
253
|
+
.replace(/\\/g, '/')
|
|
254
|
+
.replace(/\/+/g, '/')
|
|
255
|
+
.replace(/^\.\//, '')
|
|
256
|
+
.replace(/^\//, '')
|
|
257
|
+
return norm(fileA) === norm(fileB)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const checkHTMLFallback = (filename: string): boolean => {
|
|
261
|
+
if (!filename.endsWith('.html')) return false
|
|
262
|
+
const normFile = filename.startsWith('.')
|
|
263
|
+
? filename.substring(1)
|
|
264
|
+
: filename.startsWith('/')
|
|
265
|
+
? filename
|
|
266
|
+
: `/${filename}`
|
|
267
|
+
const p = location.pathname
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
p === normFile ||
|
|
271
|
+
`${p}.html` === normFile ||
|
|
272
|
+
(p.endsWith('/') ? `${p}index.html` : `${p}/index.html`) === normFile
|
|
273
|
+
)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const checkSelfPage = (filename: string): boolean => {
|
|
277
|
+
const currentRouteFile = (window as any).Bakery?.params()?.__file
|
|
278
|
+
if (currentRouteFile) {
|
|
279
|
+
return isSameFile(filename, currentRouteFile)
|
|
280
|
+
}
|
|
281
|
+
return checkHTMLFallback(filename)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const handleCSSUpdate = (filename: string) => {
|
|
285
|
+
const normCssFile = filename.startsWith('.')
|
|
286
|
+
? filename.substring(1)
|
|
287
|
+
: filename.startsWith('/')
|
|
288
|
+
? filename
|
|
289
|
+
: `/${filename}`
|
|
290
|
+
console.log(`[LiveReload] CSS change detected: ${filename}`)
|
|
291
|
+
const links = document.querySelectorAll(
|
|
292
|
+
'link[rel="stylesheet"]:not([data-removing])',
|
|
293
|
+
) as NodeListOf<HTMLLinkElement>
|
|
294
|
+
for (const link of links) {
|
|
295
|
+
const url = new URL(link.href, location.href)
|
|
296
|
+
if (url.origin === location.origin && url.pathname === normCssFile) {
|
|
297
|
+
link.setAttribute('data-removing', 'true')
|
|
298
|
+
url.searchParams.set('v', String(Date.now()))
|
|
299
|
+
const newHref = url.pathname + url.search
|
|
300
|
+
void fetch(newHref, { mode: 'no-cors' }).then(() => {
|
|
301
|
+
const newLink = document.createElement('link')
|
|
302
|
+
newLink.rel = 'stylesheet'
|
|
303
|
+
newLink.href = newHref
|
|
304
|
+
document.head.appendChild(newLink)
|
|
305
|
+
setTimeout(() => link.remove(), 50)
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const handleHtmlOrTsxUpdate = (filename: string) => {
|
|
312
|
+
if (document.visibilityState !== 'visible') {
|
|
313
|
+
needsReload = true
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const isHtmlOrTsx = filename.endsWith('.html') || filename.endsWith('.tsx')
|
|
318
|
+
if (isHtmlOrTsx) {
|
|
319
|
+
fetch(location.href)
|
|
320
|
+
.then(res => res.text())
|
|
321
|
+
.then(newHtml => {
|
|
322
|
+
const diffPercent = getHtmlDifference(
|
|
323
|
+
document.documentElement.outerHTML,
|
|
324
|
+
newHtml,
|
|
325
|
+
)
|
|
326
|
+
if (diffPercent < 15) {
|
|
327
|
+
const parser = new DOMParser()
|
|
328
|
+
const newDoc = parser.parseFromString(newHtml, 'text/html')
|
|
329
|
+
patchDOM(document.body, newDoc.body)
|
|
330
|
+
console.log(
|
|
331
|
+
`[LiveReload] Hot-swapped DOM body (${diffPercent.toFixed(1)}% change)`,
|
|
332
|
+
)
|
|
333
|
+
} else {
|
|
334
|
+
console.log(
|
|
335
|
+
`[LiveReload] Large change detected (${diffPercent.toFixed(1)}%), reloading...`,
|
|
336
|
+
)
|
|
337
|
+
location.reload()
|
|
338
|
+
}
|
|
339
|
+
})
|
|
340
|
+
.catch(() => {
|
|
341
|
+
location.reload()
|
|
342
|
+
})
|
|
343
|
+
} else {
|
|
344
|
+
location.reload()
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const handleUpdate = (filename: string) => {
|
|
349
|
+
const isCSS = filename.endsWith('.css')
|
|
350
|
+
const isSelfPage = checkSelfPage(filename)
|
|
351
|
+
const isOtherHTML = filename.endsWith('.html') && !isSelfPage
|
|
352
|
+
|
|
353
|
+
if (isOtherHTML) return
|
|
354
|
+
|
|
355
|
+
if (isCSS) {
|
|
356
|
+
handleCSSUpdate(filename)
|
|
357
|
+
} else {
|
|
358
|
+
handleHtmlOrTsxUpdate(filename)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
ws.onmessage = e => {
|
|
363
|
+
const data = typeof e.data === 'string' ? e.data : String(e.data)
|
|
364
|
+
|
|
365
|
+
// The server sends two frame shapes: legacy plain strings (a
|
|
366
|
+
// watcher-relative filename, or the literal 'force_reload') and JSON
|
|
367
|
+
// objects, currently `{type: 'error', title, body}` from
|
|
368
|
+
// compiler/dev-service.ts's notifyError. A relative path can never begin
|
|
369
|
+
// with '{', so the brace is a sound discriminator.
|
|
370
|
+
if (data.startsWith('{')) {
|
|
371
|
+
let frame: { type?: string; title?: unknown; body?: unknown } | null =
|
|
372
|
+
null
|
|
373
|
+
try {
|
|
374
|
+
frame = JSON.parse(data)
|
|
375
|
+
} catch {
|
|
376
|
+
// A '{'-prefixed frame that is not JSON is not a filename either;
|
|
377
|
+
// frame stays null and the string falls through as a legacy frame.
|
|
378
|
+
}
|
|
379
|
+
if (frame) {
|
|
380
|
+
if (frame.type === 'error') {
|
|
381
|
+
showOverlay(
|
|
382
|
+
String(frame.title ?? 'Dev server error'),
|
|
383
|
+
String(frame.body ?? ''),
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
// Unknown JSON frame types are ignored: older clients surviving a
|
|
387
|
+
// framework upgrade must not treat new frames as filenames.
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Any successful reload frame supersedes whatever error was on screen.
|
|
393
|
+
hideOverlay()
|
|
394
|
+
handleUpdate(data)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
ws.onopen = () => {
|
|
398
|
+
reconnectAttempts = 0
|
|
399
|
+
hideOverlay()
|
|
400
|
+
|
|
401
|
+
while (logQueue.length > 0) {
|
|
402
|
+
ws.send(logQueue.shift()!)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (isDead) {
|
|
406
|
+
console.log('[LiveReload] Server is back! Refreshing...')
|
|
407
|
+
location.reload()
|
|
408
|
+
} else {
|
|
409
|
+
console.log('[LiveReload] Connected')
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
ws.onclose = () => {
|
|
414
|
+
isDead = true
|
|
415
|
+
// Back off with jitter. A flat 1s retry meant every open tab hammered a
|
|
416
|
+
// stopped dev server once a second, indefinitely and in lockstep.
|
|
417
|
+
const delay = Math.min(1000 * 2 ** reconnectAttempts, 30_000)
|
|
418
|
+
reconnectAttempts += 1
|
|
419
|
+
// A dead dev server used to mean silent reconnect attempts — the page just
|
|
420
|
+
// quietly stopped reloading. After a few failures (~7s of downtime with
|
|
421
|
+
// the backoff above) say so; onopen dismisses it and reloads on reconnect.
|
|
422
|
+
if (reconnectAttempts > 3) {
|
|
423
|
+
showOverlay('dev server disconnected', 'waiting to reconnect…')
|
|
424
|
+
}
|
|
425
|
+
setTimeout(connect, delay + Math.random() * 500)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
ws.onerror = () => ws.close()
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
connect()
|
|
432
|
+
|
|
433
|
+
document.addEventListener('visibilitychange', () => {
|
|
434
|
+
if (document.visibilityState === 'visible' && needsReload) {
|
|
435
|
+
location.reload()
|
|
436
|
+
}
|
|
437
|
+
})
|