-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathserver.js
245 lines (209 loc) · 5.64 KB
/
server.js
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
// native
import { readFileSync } from 'node:fs';
import { createServer } from 'node:http';
// packages
import access from 'local-access';
import polka from 'polka';
import sirv from 'sirv';
/**
* Constant for repeated pinging.
*
* @private
* @type {string}
*/
const ping = ':ping\n';
/**
* A helper for sending SSE messages.
*
* @private
* @param {string[]} messages A list of messages to send
* @return {string}
*/
function writeMessage(messages) {
return messages.join('\n') + '\n\n';
}
/**
* Constant for reused Cache-Control header.
*
* @private
* @type {string}
*/
const doNotCache = 'no-cache, no-store, must-revalidate';
/**
* The contents of the compiled client-side code as a Buffer.
* @private
* @type {Buffer}
*/
const clientScript = readFileSync(
new URL('client/dist/client.js', import.meta.url)
);
/**
* The favicon.ico as a Buffer.
* @private
* @type {Buffer}
*/
const favicon = readFileSync(new URL('assets/favicon.ico', import.meta.url));
/**
* What's returned when the `create` function is called.
*
* @typedef {object} CreateReturn
* @property {Function} close Stops the server if it is running
* @property {Function} reload When called this function will reload any connected HTML documents, can accept the path to a file to target for reload
* @property {Function} start When called the server will begin running
*/
/**
* What's returned by the `start` function in a Promise.
*
* @typedef {object} StartReturn
* @property {string} local The localhost URL for the static site
* @property {string} network The local networked URL for the static site
* @property {number} port The port the server ended up on
*/
/**
* Creates a server on the preferred port and begins serving the provided
* directories locally.
*
* @param {object} options
* @param {string|string[]} [options.dir] The directory or list of directories to serve
* @param {number} [options.port] The port to serve on
* @return {CreateReturn}
* @example
* const { create } = require('mini-sync');
*
* const server = create({ dir: 'app', port: 3000 });
*
* const { local } = await server.start();
*
* console.log(`Now serving at: ${local}`);
*
* // any time a file needs to change, use "reload"
* server.reload('app.css');
*
* // reloads the whole page
* server.reload();
*
* // close the server
* await server.close();
*
*/
export function create({ dir = process.cwd(), port = 3000 } = {}) {
// create a raw instance of http.Server so we can hook into it
const server = createServer();
// a Set to track all the current client connections
const clients = new Set();
// create our polka server
const app = polka({ server });
// make sure "serve" is an array
const toWatch = Array.isArray(dir) ? dir : [dir];
// add each directory in "serve"
for (let idx = 0; idx < toWatch.length; idx++) {
const directory = toWatch[idx];
app.use(sirv(directory, { dev: true }));
}
app.get('/__mini_sync__', (req, res) => {
//send headers for event-stream connection
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': doNotCache,
'Connection': 'keep-alive',
});
// send initial ping with retry command
res.write('retry: 10000\n');
// add client to Set
clients.add(res);
function disconnect() {
// close the response
res.end();
// remove client from our Set
clients.delete(res);
}
req.on('error', disconnect);
res.on('error', disconnect);
res.on('close', disconnect);
res.on('finish', disconnect);
});
app.get('__mini_sync__/client.js', (_, res) => {
// send headers for shipping down a JS file
res.writeHead(200, {
'Cache-Control': doNotCache,
'Content-Length': clientScript.byteLength,
'Content-Type': 'text/javascript',
});
// send the client-side script Buffer
res.end(clientScript);
});
app.get('/favicon.ico', (_, res) => {
// send headers for shipping down favicon
res.writeHead(200, {
'Cache-Control': doNotCache,
'Content-Length': favicon.byteLength,
'Content-Type': 'image/vnd.microsoft.icon',
});
res.end(favicon);
});
/**
* Tells all connected clients to reload.
*
* @param {string} [file] The file to reload
*/
function reload(file) {
for (const client of clients) {
client.write(
writeMessage(['event: reload', `data: ${JSON.stringify({ file })}`])
);
}
}
function sendPing() {
for (const client of clients) {
client.write(ping);
}
}
/**
* Returns a promise once the server closes.
*
* @returns {Promise<void>}
*/
function close() {
return new Promise((resolve, reject) => {
server.close((err) => {
if (err) reject(err);
resolve();
});
});
}
/**
* Returns a promise once the server starts.
*
* @returns {Promise<StartReturn>}
*/
function start() {
return new Promise((resolve, reject) => {
server.on('error', (e) => {
if (e.code === 'EADDRINUSE') {
setTimeout(() => {
server.close();
server.listen(++port);
}, 100);
} else {
reject(e);
}
});
let interval;
server.on('listening', () => {
// ping every 10 seconds
interval = setInterval(sendPing, 10e3);
// get paths to networks
const { local, network } = access({ port });
resolve({ local, network, port });
});
server.on('close', () => {
if (interval) {
clearInterval(interval);
interval = null;
}
});
app.listen(port);
});
}
return { close, reload, start };
}