-
Notifications
You must be signed in to change notification settings - Fork 3
/
index.ts
117 lines (100 loc) · 2.84 KB
/
index.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
declare global {
var clients: Set<ReadableStreamDefaultController> | undefined;
var autoReload: boolean | undefined;
}
globalThis.clients ??= new Set<ReadableStreamDefaultController>();
globalThis.autoReload = globalThis.autoReload ?? true;
export function reloadClients(): void {
if (globalThis.clients !== undefined) {
for (const client of globalThis.clients) {
client.enqueue("data: RELOAD\n\n");
}
}
}
if (globalThis.autoReload) {
reloadClients();
}
export type LiveReloadOptions = {
/**
* URL path used for server-sent events
* @default "__dev__/reload"
*/
readonly eventPath?: string;
/**
* URL path used for live reload script
* @default "__dev__/ws"
*/
readonly scriptPath?: string;
};
type Fetch = (req: Request) => Promise<Response>;
/**
* Automatically reload html when Bun server hot reloads
*
* @param fetch Bun server's fetch function
* @param options Live reload options
*
* @returns fetch function with live reload
*/
export function withHtmlLiveReload(
handler: Fetch,
options?: {
eventPath?: string;
scriptPath?: string;
autoReload?: false;
},
): Fetch {
if (options?.autoReload === false) {
globalThis.autoReload = false;
}
return async (req): Promise<Response> => {
if (req.method !== "GET") {
return handler(req);
}
const requestUrl = new URL(req.url);
const { eventPath, scriptPath } = {
eventPath: "/__dev__/reload",
scriptPath: "/__dev__/reload.js",
...options,
};
if (requestUrl.pathname === eventPath) {
const stream = new ReadableStream({
start(controller): void {
globalThis.clients?.add(controller);
req.signal.addEventListener("abort", () => {
controller.close();
globalThis.clients?.delete(controller);
});
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
},
});
}
if (requestUrl.pathname === scriptPath) {
return new Response(
`new EventSource("${eventPath}").onmessage = function(msg) {
if(msg.data === 'RELOAD') { location.reload(); }
};`,
{ headers: { "Content-Type": "text/javascript" } },
);
}
const response = await handler(req);
const contentType = response.headers.get("Content-Type");
const isResponseHtml = contentType?.startsWith("text/html") ?? false;
if (!isResponseHtml) {
return response;
}
const liveReloadScript = `<script type="module" src="${scriptPath}"></script>`;
const output = new HTMLRewriter()
.onDocument({
end: (el): void => {
el.append(liveReloadScript, { html: true });
},
})
.transform(response);
return new Response(await output.blob(), output);
};
}