-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathlive_deno.mjs
152 lines (127 loc) · 4.31 KB
/
live_deno.mjs
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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/*
Implementation of "live reloading". Provides a broadcaster that can be
plugged into any Deno server. The broadcaster serves its client script,
maintains client connections, and broadcasts events.
Use `LiveDirs` to both serve files and filter FS events. This avoids duplication
between your file-serving and watch-filtering configs.
Simplified example of sending notifications to clients:
import * as ld from '{{url}}/live_deno.mjs'
import * as hd from '{{url}}/http_deno.mjs'
const dirs = ld.LiveDirs.of(new hd.DirRel(`.`))
const bro = new ld.LiveBroad()
for await (const event of dirs.watchLive()) {
await bro.writeEventJson(event)
}
*/
import * as lc from './live_client.mjs'
import * as l from './lang.mjs'
import * as h from './http.mjs'
import * as hd from './http_deno.mjs'
import * as hs from './http_srv.mjs'
import * as io from './io_deno.mjs'
import * as p from './path.mjs'
export const LIVE_PATH = `/e8f2dcbe89994b14a1a1c59c2ea6eac7`
export class LiveBroad extends hs.Broad {
get basePath() {return LIVE_PATH}
get clientPath() {return p.posix.join(this.basePath, `live_client.mjs`)}
get eventsPath() {return p.posix.join(this.basePath, `events`)}
get sendPath() {return p.posix.join(this.basePath, `send`)}
res(val) {
const rou = h.toReqRou(val)
if (rou.get(this.clientPath)) return this.clientRes(rou)
if (rou.get(this.eventsPath)) return this.eventsRes(rou)
if (rou.post(this.sendPath)) return this.sendRes(rou)
return undefined
}
clientRes() {
return new Response(
`void ${lc.main.toString()}()`,
{headers: HEADERS_LIVE_SCRIPT},
)
}
eventsRes(rou) {
return new Response(
this.make(rou.req.signal),
{headers: HEADERS_LIVE_EVENT_STREAM},
)
}
async sendRes(rou) {
await this.writeEvent(await rou.req.text())
return rou.empty()
}
onWriteErr(err) {
if (hd.isErrCancel(err) || hs.isStreamWriteErr(err)) return
throw err
}
}
const HEADERS_LIVE_SCRIPT = [
...h.HEADERS_CORS_PROMISCUOUS,
[h.HEADER_NAME_CONTENT_TYPE, `application/javascript`],
]
const HEADERS_LIVE_EVENT_STREAM = [
...h.HEADERS_CORS_PROMISCUOUS,
[h.HEADER_NAME_CONTENT_TYPE, `text/event-stream`],
[`transfer-encoding`, `utf-8`],
]
export function dirs(...val) {return LiveDirs.of(...val)}
export class LiveDirs extends hd.Dirs {
/*
Watches the current working directory, converting Deno FS events to events
understood by the live client:
* Convert "kind" to "type".
* Flatten "paths" into "path".
* Convert absolute path to relative.
* Convert Windows-style to Posix-style.
* Resolve against relative directories.
* Test against directory tests.
The resulting events can be sent to the live client.
*/
async *watchLive() {
for await (const {kind: type, paths} of io.watchCwd()) {
for (let path of paths) {
path = this.fsPathToUrlPath(path)
if (l.isSome(path)) yield {type, path}
}
}
}
}
/*
Takes a URL for the "live" client script and an arbitrary response. If the
response is HTML, returns a modified response with an HTML script tag that
loads the live client. Otherwise returns the response as-is.
The client URL can be acquired from a `LiveBroad` instance, constructed via URL
tools, hardcoded, etc.
*/
export function withLiveClient(url, res) {
if (isResHtml(res)) {
return new Response(
io.ConcatStreamSource.stream(res.body, script(url)),
res,
)
}
return res
}
/*
Interpolating arbitrary strings into HTML is a horrible malpractice. NEVER do
this in production code. ALWAYS use structured markup tools like our `prax.mjs`
module. However, in this case it would be an excessive dependency for such a
small use case. This is a development tool, the input should be trusted and
shouldn't contain characters that require escaping.
*/
function script(src) {
return `<script type="module" src="${l.render(src)}"></script>`
}
// May consider moving to `http_srv.mjs` later.
function isResHtml(res) {
return (
l.isInst(res, Response) &&
contentTypeMatch(res?.headers.get(h.HEADER_NAME_CONTENT_TYPE), h.MIME_TYPE_HTML)
)
}
// May consider moving to `http_srv.mjs` later.
function contentTypeMatch(src, exp) {
src = strNorm(src)
exp = strNorm(exp)
return src === exp || src.startsWith(exp + `;`)
}
function strNorm(val) {return l.laxStr(val).trim().toLowerCase()}