@uniflowed/server 0.0.0-alpha.5 → 0.0.0-alpha.7
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/package.json +4 -2
- package/standalone.js +436 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/server",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.7",
|
|
4
4
|
"description": "Request-scoped server functions for the Unified Toolchain for Flow (React).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
},
|
|
13
13
|
"exports": {
|
|
14
14
|
".": "./index.js",
|
|
15
|
-
"./host": "./host.js"
|
|
15
|
+
"./host": "./host.js",
|
|
16
|
+
"./standalone": "./standalone.js"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
18
19
|
"index.js",
|
|
19
20
|
"host.js",
|
|
21
|
+
"standalone.js",
|
|
20
22
|
"internal/*.js"
|
|
21
23
|
]
|
|
22
24
|
}
|
package/standalone.js
ADDED
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/server/standalone`: the application, serving itself, from one file.
|
|
4
|
+
//
|
|
5
|
+
// `uf build --compile` links this module with the project's server bundle and
|
|
6
|
+
// with an embedded copy of everything `uf build` wrote to `dist/`, then hands
|
|
7
|
+
// the result to a JavaScript runtime that appends itself to it. What comes out
|
|
8
|
+
// is a file that can be copied into an empty directory and run: no Node
|
|
9
|
+
// installation, no `node_modules`, no `dist/` beside it.
|
|
10
|
+
//
|
|
11
|
+
// # Why `node:http`, and not the host's own server
|
|
12
|
+
//
|
|
13
|
+
// Bun has `Bun.serve` and Deno has `Deno.serve`, and both are faster than the
|
|
14
|
+
// interface they emulate. Neither is a standard. Writing this file against
|
|
15
|
+
// either would make it a Bun file or a Deno file, and the runtime that gets
|
|
16
|
+
// embedded would stop being a decision `uf build --compile` makes and start
|
|
17
|
+
// being a decision this module already made. `node:http` is the one server
|
|
18
|
+
// interface all three hosts implement, so it is the one that leaves the choice
|
|
19
|
+
// open — which matters more here than the throughput of a shim that spends
|
|
20
|
+
// almost all of its time inside React.
|
|
21
|
+
//
|
|
22
|
+
// # Why this is not `@uniflowed/vite`'s `internal/serve.js`
|
|
23
|
+
//
|
|
24
|
+
// That module is the handler behind `uf preview` and `uf start`, and it is the
|
|
25
|
+
// obvious thing to import rather than write a second one. It is the wrong
|
|
26
|
+
// thing to import, for two reasons and either would be enough.
|
|
27
|
+
//
|
|
28
|
+
// It answers by opening files under `dist/`, and there is no `dist/` here —
|
|
29
|
+
// the whole claim of a compiled binary is that it was copied into an empty
|
|
30
|
+
// directory. Its static half is therefore not shareable at all, and its
|
|
31
|
+
// application half arrives attached to it. And it lives in `@uniflowed/vite`,
|
|
32
|
+
// so importing it would link the package named after the bundler into the
|
|
33
|
+
// artefact a deployment runs, which is the property `uf start` exists to
|
|
34
|
+
// establish and the one a single file makes strongest.
|
|
35
|
+
//
|
|
36
|
+
// So the code is not shared and the *answer* is. Both resolve a request in the
|
|
37
|
+
// same order — a file the build already wrote, then a route handler for any
|
|
38
|
+
// method, then a render for whatever is left — and that order is not a
|
|
39
|
+
// preference either module gets to hold: `uf preview` is Vite's own server,
|
|
40
|
+
// which runs its file middleware before anything uf mounts behind it, so
|
|
41
|
+
// `internal/serve.js` matches Vite and this matches `internal/serve.js`. A
|
|
42
|
+
// binary that resolved a page/handler collision the other way would behave
|
|
43
|
+
// one way when it was checked with `uf preview` and another way once it was
|
|
44
|
+
// deployed, which is the trap `uf preview` exists to prevent.
|
|
45
|
+
//
|
|
46
|
+
// Both copies are driven by a test: `serve.test.js` and
|
|
47
|
+
// `preview_and_start_serve_the_whole_of_a_build` for that one,
|
|
48
|
+
// `tests/library/standalone.test.js` and
|
|
49
|
+
// `compile_writes_one_file_that_serves_the_site_from_an_empty_directory` for
|
|
50
|
+
// this one.
|
|
51
|
+
|
|
52
|
+
import { Buffer } from "node:buffer";
|
|
53
|
+
import { createServer } from "node:http";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The pieces of a Node request and response this module touches.
|
|
57
|
+
*
|
|
58
|
+
* Declared structurally rather than imported from a `node:http` libdef,
|
|
59
|
+
* because the set is five members wide and naming it here is what lets the
|
|
60
|
+
* same file be read without knowing which host's types are in scope.
|
|
61
|
+
*/
|
|
62
|
+
type NodeRequest = {
|
|
63
|
+
readonly method?: string,
|
|
64
|
+
readonly url?: string,
|
|
65
|
+
readonly headers: { readonly [string]: string | Array<string> | void },
|
|
66
|
+
...
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type NodeResponse = {
|
|
70
|
+
statusCode: number,
|
|
71
|
+
setHeader(name: string, value: string): mixed,
|
|
72
|
+
write(chunk: Uint8Array | string): mixed,
|
|
73
|
+
end(chunk?: Uint8Array | string): mixed,
|
|
74
|
+
...
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** One file from `dist/`, as `uf build --compile` embedded it. */
|
|
78
|
+
export type EmbeddedAsset = {|
|
|
79
|
+
/** The `content-type` to serve it with, decided at build time. */
|
|
80
|
+
readonly type: string,
|
|
81
|
+
/** The file's bytes, base64. */
|
|
82
|
+
readonly body: string,
|
|
83
|
+
|};
|
|
84
|
+
|
|
85
|
+
/** Every embedded file, keyed by its path relative to the output directory. */
|
|
86
|
+
export type EmbeddedAssets = { readonly [path: string]: EmbeddedAsset };
|
|
87
|
+
|
|
88
|
+
/** The script, stylesheet and preload URLs a rendered document references. */
|
|
89
|
+
export type DocumentAssets = {|
|
|
90
|
+
readonly scripts: $ReadOnlyArray<string>,
|
|
91
|
+
readonly styles: $ReadOnlyArray<string>,
|
|
92
|
+
readonly preloads: $ReadOnlyArray<string>,
|
|
93
|
+
|};
|
|
94
|
+
|
|
95
|
+
/** What the project's server bundle exports; see `virtual:uf/server`. */
|
|
96
|
+
export type StandaloneApp = {|
|
|
97
|
+
readonly render: (
|
|
98
|
+
url: string,
|
|
99
|
+
assets: DocumentAssets,
|
|
100
|
+
) => Promise<{|
|
|
101
|
+
readonly status: number,
|
|
102
|
+
readonly html: string,
|
|
103
|
+
readonly headers?: { readonly [string]: string },
|
|
104
|
+
|}>,
|
|
105
|
+
readonly dispatch: (request: Request) => Promise<Response | null>,
|
|
106
|
+
|};
|
|
107
|
+
|
|
108
|
+
/** Everything an application needs to answer a request, all of it built in. */
|
|
109
|
+
export type HandlerOptions = {|
|
|
110
|
+
readonly app: StandaloneApp,
|
|
111
|
+
readonly assets: EmbeddedAssets,
|
|
112
|
+
readonly document: DocumentAssets,
|
|
113
|
+
|};
|
|
114
|
+
|
|
115
|
+
/** What [`serve`] needs: the above, and where to listen. */
|
|
116
|
+
export type ServeOptions = {|
|
|
117
|
+
readonly app: StandaloneApp,
|
|
118
|
+
readonly assets: EmbeddedAssets,
|
|
119
|
+
readonly document: DocumentAssets,
|
|
120
|
+
/** Overridden by `--port` and then by `PORT`; defaults to 3000. */
|
|
121
|
+
readonly port?: number,
|
|
122
|
+
/** Overridden by `--host` and then by `HOST`; defaults to loopback. */
|
|
123
|
+
readonly host?: string,
|
|
124
|
+
|};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* How long a browser may keep a file that is not a document.
|
|
128
|
+
*
|
|
129
|
+
* One hour, uniformly, and deliberately not the year-long `immutable` a
|
|
130
|
+
* content-hashed chunk could take: this module cannot tell a hashed chunk from
|
|
131
|
+
* an unhashed file copied out of `public/`, because `dist/` records no such
|
|
132
|
+
* distinction, and getting it wrong in the `immutable` direction pins a stale
|
|
133
|
+
* favicon in every visitor's cache with no way to recall it. A binary behind a
|
|
134
|
+
* CDN should let the CDN decide; an hour is the safe answer for one that is not.
|
|
135
|
+
*/
|
|
136
|
+
const ASSET_CACHE_CONTROL = "public, max-age=3600";
|
|
137
|
+
|
|
138
|
+
/** Documents are revalidated every time, because a deploy replaces them. */
|
|
139
|
+
const DOCUMENT_CACHE_CONTROL = "no-cache";
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Serve the application until the process is stopped.
|
|
143
|
+
*
|
|
144
|
+
* Resolves once the socket is listening, with the address it took — a caller
|
|
145
|
+
* that asked for port 0 has no other way to learn which port it got, and the
|
|
146
|
+
* test that drives a compiled binary needs exactly that.
|
|
147
|
+
*/
|
|
148
|
+
export async function serve(options: ServeOptions): Promise<{|
|
|
149
|
+
readonly host: string,
|
|
150
|
+
readonly port: number,
|
|
151
|
+
readonly close: () => Promise<void>,
|
|
152
|
+
|}> {
|
|
153
|
+
const handle = createHandler(options);
|
|
154
|
+
|
|
155
|
+
// Said before the socket, not after it, and that ordering is the point. It
|
|
156
|
+
// is the one fact about a compiled binary that cannot be checked from
|
|
157
|
+
// outside it — whether `dist/` really came along — and a count printed only
|
|
158
|
+
// on a successful bind is a count nobody can see on a machine that is not
|
|
159
|
+
// allowed to bind. `compile_writes_one_file_that_carries_the_whole_site`
|
|
160
|
+
// reads this line and nothing else.
|
|
161
|
+
process.stdout.write(`uf: ${String(Object.keys(options.assets).length)} embedded files\n`);
|
|
162
|
+
|
|
163
|
+
const server = createServer((request: NodeRequest, response: NodeResponse) => {
|
|
164
|
+
handle(request, response).catch((error: mixed) => {
|
|
165
|
+
// A request that throws is this server's last chance to say so: there is
|
|
166
|
+
// no framework above it and no log drain beside it. Report it on stderr
|
|
167
|
+
// and answer 500, rather than letting the host's unhandled-rejection
|
|
168
|
+
// policy decide whether the process survives.
|
|
169
|
+
process.stderr.write(`uf: ${String(error?.stack ?? error)}\n`);
|
|
170
|
+
try {
|
|
171
|
+
response.statusCode = 500;
|
|
172
|
+
response.setHeader("content-type", "text/plain; charset=utf-8");
|
|
173
|
+
response.end("internal server error\n");
|
|
174
|
+
} catch {
|
|
175
|
+
// The response was already partly written; nothing left to say.
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const host = options.host ?? argument("--host") ?? process.env.HOST ?? "127.0.0.1";
|
|
181
|
+
const port = options.port ?? Number(argument("--port") ?? process.env.PORT ?? 3000);
|
|
182
|
+
|
|
183
|
+
await new Promise((resolve, reject) => {
|
|
184
|
+
server.once("error", reject);
|
|
185
|
+
server.listen(port, host, resolve);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const address = server.address();
|
|
189
|
+
const bound = typeof address === "object" && address != null ? address.port : port;
|
|
190
|
+
process.stdout.write(`uf: listening on http://${host}:${String(bound)}\n`);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
host,
|
|
194
|
+
port: bound,
|
|
195
|
+
close: () =>
|
|
196
|
+
new Promise((resolve) => {
|
|
197
|
+
server.close(() => resolve());
|
|
198
|
+
}),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The embedded files, as a map.
|
|
204
|
+
*
|
|
205
|
+
* A `Map` rather than the generated object itself, because a lookup keyed by a
|
|
206
|
+
* request path must not be able to find `__proto__` or `constructor`. Building
|
|
207
|
+
* it costs one pass over a few hundred entries at startup and removes the
|
|
208
|
+
* question entirely.
|
|
209
|
+
*
|
|
210
|
+
* The bytes are decoded lazily and then kept: a build that embeds a hundred
|
|
211
|
+
* megabytes of sourcemaps should not spend the startup decoding the ones this
|
|
212
|
+
* process will never be asked for.
|
|
213
|
+
*/
|
|
214
|
+
function index(assets: EmbeddedAssets): Map<string, {| +type: string, +bytes: () => Buffer |}> {
|
|
215
|
+
const files = new Map();
|
|
216
|
+
for (const path of Object.keys(assets)) {
|
|
217
|
+
const asset = assets[path];
|
|
218
|
+
let decoded: Buffer | null = null;
|
|
219
|
+
files.set(path, {
|
|
220
|
+
type: asset.type,
|
|
221
|
+
bytes: () => {
|
|
222
|
+
if (decoded == null) {
|
|
223
|
+
// `Buffer.from` rather than `atob`: `atob` answers with a string of
|
|
224
|
+
// char codes, and turning megabytes of that into bytes is a loop in
|
|
225
|
+
// JavaScript. Every host that has `node:http` has `node:buffer`.
|
|
226
|
+
decoded = Buffer.from(asset.body, "base64");
|
|
227
|
+
}
|
|
228
|
+
return decoded;
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return files;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* One request, answered — exported so an application can be mounted rather
|
|
237
|
+
* than only run.
|
|
238
|
+
*
|
|
239
|
+
* `serve` is the whole of a compiled binary, and it is not the whole of what
|
|
240
|
+
* anybody wants: a uf application behind an existing Node server, or beside
|
|
241
|
+
* other routes in one process, needs the request handling without the socket.
|
|
242
|
+
* That is this. It is also what the tests drive, which is not a coincidence —
|
|
243
|
+
* a request handler that can only be reached through a listening socket is one
|
|
244
|
+
* that can only be tested on a machine allowed to bind one.
|
|
245
|
+
*
|
|
246
|
+
* The order is `internal/serve.js`'s, which is Vite's:
|
|
247
|
+
*
|
|
248
|
+
* 1. a file `uf build` already wrote, for `GET` and `HEAD` only — an
|
|
249
|
+
* embedded path that matches exactly, such as `/assets/index-a1b2c3.js`
|
|
250
|
+
* or anything copied out of `public/`, and then the prerendered document
|
|
251
|
+
* for this URL, because `/guide` was written as `guide/index.html`;
|
|
252
|
+
* 2. a route handler, for any method, because a handler is the only thing
|
|
253
|
+
* that answers a `POST` and may also answer a `GET` for a path with no
|
|
254
|
+
* page;
|
|
255
|
+
* 3. and otherwise the renderer, which also produces the 404.
|
|
256
|
+
*
|
|
257
|
+
* The prerendered document is looked up *before* the dispatcher, and that is
|
|
258
|
+
* the one place this used to disagree with `uf preview` and `uf start`. The
|
|
259
|
+
* router allows a handler to sit beside a page in the same directory, so a
|
|
260
|
+
* path can have both — and Vite's preview server serves the file first with no
|
|
261
|
+
* say in the matter, so a binary that let the handler win would answer
|
|
262
|
+
* differently from the command a build is checked with. Answering the same
|
|
263
|
+
* wrong-looking way as the other two is worth more than answering a better way
|
|
264
|
+
* alone.
|
|
265
|
+
*
|
|
266
|
+
* A page never answers a `POST`: letting one try turns a missing handler into
|
|
267
|
+
* a rendered page with a 200 where the caller expected a 405.
|
|
268
|
+
*/
|
|
269
|
+
export function createHandler(
|
|
270
|
+
options: HandlerOptions,
|
|
271
|
+
): (NodeRequest, NodeResponse) => Promise<void> {
|
|
272
|
+
const { app, document } = options;
|
|
273
|
+
const files = index(options.assets);
|
|
274
|
+
|
|
275
|
+
return async function handle(request: NodeRequest, response: NodeResponse): Promise<void> {
|
|
276
|
+
const method = (request.method ?? "GET").toUpperCase();
|
|
277
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
278
|
+
const pathname = decodePath(url.pathname);
|
|
279
|
+
|
|
280
|
+
if (pathname != null && (method === "GET" || method === "HEAD")) {
|
|
281
|
+
const file = files.get(assetKey(pathname));
|
|
282
|
+
if (file != null) {
|
|
283
|
+
sendBytes(response, method, 200, file.type, ASSET_CACHE_CONTROL, file.bytes());
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
// A document is revalidated where an asset is cached, because a deploy
|
|
287
|
+
// replaces documents and gives assets a new hashed name.
|
|
288
|
+
const page = files.get(documentKey(pathname));
|
|
289
|
+
if (page != null) {
|
|
290
|
+
sendBytes(response, method, 200, page.type, DOCUMENT_CACHE_CONTROL, page.bytes());
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const handled = await app.dispatch(toRequest(request, url));
|
|
296
|
+
if (handled != null) {
|
|
297
|
+
await send(response, method, handled);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
302
|
+
// A page supports exactly `GET` and `HEAD`, which is why the `Allow` the
|
|
303
|
+
// specification requires on every 405 can be written here even though
|
|
304
|
+
// this side of the handler knows nothing about methods. A *handler* path
|
|
305
|
+
// with the wrong method never reaches this line: the dispatcher answers
|
|
306
|
+
// that one itself, with the methods that module really exports.
|
|
307
|
+
response.setHeader("allow", "GET, HEAD");
|
|
308
|
+
sendBytes(
|
|
309
|
+
response,
|
|
310
|
+
method,
|
|
311
|
+
405,
|
|
312
|
+
"text/plain; charset=utf-8",
|
|
313
|
+
DOCUMENT_CACHE_CONTROL,
|
|
314
|
+
Buffer.from("method not allowed\n"),
|
|
315
|
+
);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const rendered = await app.render(url.pathname + url.search, document);
|
|
320
|
+
response.statusCode = rendered.status;
|
|
321
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
322
|
+
response.setHeader("cache-control", DOCUMENT_CACHE_CONTROL);
|
|
323
|
+
for (const name of Object.keys(rendered.headers ?? {})) {
|
|
324
|
+
response.setHeader(name, (rendered.headers ?? {})[name]);
|
|
325
|
+
}
|
|
326
|
+
const html = Buffer.from(rendered.html, "utf8");
|
|
327
|
+
response.setHeader("content-length", String(html.byteLength));
|
|
328
|
+
response.end(method === "HEAD" ? undefined : html);
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* A request path as an embedded key, or `null` when it cannot be one.
|
|
334
|
+
*
|
|
335
|
+
* Percent-decoding happens here rather than at the lookup, because a path that
|
|
336
|
+
* does not decode is a malformed request and not a missing file.
|
|
337
|
+
*/
|
|
338
|
+
function decodePath(pathname: string): string | null {
|
|
339
|
+
try {
|
|
340
|
+
return decodeURIComponent(pathname);
|
|
341
|
+
} catch {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** `/assets/x.js` is the embedded `assets/x.js`. */
|
|
347
|
+
function assetKey(pathname: string): string {
|
|
348
|
+
return pathname.replace(/^\/+/, "");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* The prerendered document for a URL.
|
|
353
|
+
*
|
|
354
|
+
* `uf build` writes `/guide` as `guide/index.html`, and `/` as `index.html`,
|
|
355
|
+
* so both spellings of a directory URL find the same file.
|
|
356
|
+
*/
|
|
357
|
+
function documentKey(pathname: string): string {
|
|
358
|
+
const key = assetKey(pathname).replace(/\/+$/, "");
|
|
359
|
+
return key === "" ? "index.html" : `${key}/index.html`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* A Node request as a `Request`.
|
|
364
|
+
*
|
|
365
|
+
* The body is passed as a stream where the host allows it, so a handler that
|
|
366
|
+
* accepts an upload does not need the whole thing buffered before it starts.
|
|
367
|
+
* `duplex` is required by the specification whenever a body is a stream, and
|
|
368
|
+
* Node throws without it.
|
|
369
|
+
*/
|
|
370
|
+
function toRequest(incoming: NodeRequest, url: URL): Request {
|
|
371
|
+
const headers = new Headers();
|
|
372
|
+
for (const name of Object.keys(incoming.headers)) {
|
|
373
|
+
const value = incoming.headers[name];
|
|
374
|
+
if (value == null) continue;
|
|
375
|
+
for (const entry of Array.isArray(value) ? value : [value]) {
|
|
376
|
+
headers.append(name, entry);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const host = headers.get("host");
|
|
381
|
+
if (host != null && host !== "") {
|
|
382
|
+
url.host = host;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const method = (incoming.method ?? "GET").toUpperCase();
|
|
386
|
+
const init: { [string]: mixed } = { method, headers };
|
|
387
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
388
|
+
init.body = incoming;
|
|
389
|
+
init.duplex = "half";
|
|
390
|
+
}
|
|
391
|
+
// $FlowFixMe[incompatible-call] - `incoming` is a stream, which `Request` accepts.
|
|
392
|
+
return new Request(url, init);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Write a `Response` to a Node response. */
|
|
396
|
+
async function send(outgoing: NodeResponse, method: string, result: Response): Promise<void> {
|
|
397
|
+
outgoing.statusCode = result.status;
|
|
398
|
+
for (const [name, value] of result.headers) {
|
|
399
|
+
outgoing.setHeader(name, value);
|
|
400
|
+
}
|
|
401
|
+
if (result.body == null || method === "HEAD") {
|
|
402
|
+
outgoing.end();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
// Streamed rather than buffered, so a handler returning a large or
|
|
406
|
+
// open-ended body is not read into memory first.
|
|
407
|
+
const reader = result.body.getReader();
|
|
408
|
+
for (;;) {
|
|
409
|
+
const { done, value } = await reader.read();
|
|
410
|
+
if (done) break;
|
|
411
|
+
outgoing.write(value);
|
|
412
|
+
}
|
|
413
|
+
outgoing.end();
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Write one embedded file, with the length a client needs to reuse a socket. */
|
|
417
|
+
function sendBytes(
|
|
418
|
+
outgoing: NodeResponse,
|
|
419
|
+
method: string,
|
|
420
|
+
status: number,
|
|
421
|
+
type: string,
|
|
422
|
+
cacheControl: string,
|
|
423
|
+
bytes: Buffer,
|
|
424
|
+
): void {
|
|
425
|
+
outgoing.statusCode = status;
|
|
426
|
+
outgoing.setHeader("content-type", type);
|
|
427
|
+
outgoing.setHeader("cache-control", cacheControl);
|
|
428
|
+
outgoing.setHeader("content-length", String(bytes.byteLength));
|
|
429
|
+
outgoing.end(method === "HEAD" ? undefined : bytes);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/** The value of a `--flag value` pair on the command line, if it is there. */
|
|
433
|
+
function argument(name: string): string | null {
|
|
434
|
+
const at = process.argv.indexOf(name);
|
|
435
|
+
return at === -1 ? null : (process.argv[at + 1] ?? null);
|
|
436
|
+
}
|