@uniflowed/server 0.0.0-alpha.10

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/standalone.js ADDED
@@ -0,0 +1,593 @@
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
+ // # The one thing this door does not have
53
+ //
54
+ // The route cache. `rendering.cache.route` reaches `createFetchHandler` —
55
+ // which is `uf preview`, `uf start`, and every `--adapter` target through the
56
+ // `handler.js` uf generates — and this module renders every request whatever
57
+ // the configuration says. It is the fourth front door and the only one where
58
+ // the caching is not a shared function but a second copy of the same
59
+ // buffering, the same refusal to store a render that read the request, and the
60
+ // same header, which is exactly the "two copies that agree until they do not"
61
+ // this file's header is otherwise about. It is stated here rather than left to
62
+ // be discovered, and ubugeeei-prod/uf#277 carries it.
63
+
64
+ import { Buffer } from "node:buffer";
65
+ import { createServer } from "node:http";
66
+
67
+ import { send } from "./node.js";
68
+
69
+ /**
70
+ * The pieces of a Node request and response this module touches.
71
+ *
72
+ * Declared structurally rather than imported from a `node:http` libdef,
73
+ * because the set is five members wide and naming it here is what lets the
74
+ * same file be read without knowing which host's types are in scope.
75
+ */
76
+ type NodeRequest = {
77
+ readonly method?: string,
78
+ readonly url?: string,
79
+ readonly headers: { readonly [string]: string | Array<string> | void },
80
+ ...
81
+ };
82
+
83
+ type NodeResponse = {
84
+ statusCode: number,
85
+ setHeader(name: string, value: string): mixed,
86
+ write(chunk: Uint8Array | string): mixed,
87
+ end(chunk?: Uint8Array | string): mixed,
88
+ // Required rather than optional, because the one case it exists for is the
89
+ // one where nothing else will do: a render that fails after the shell has
90
+ // gone out cannot be answered with a status, and dropping the socket is the
91
+ // only way left to tell the client the document it received is not whole.
92
+ destroy(error?: mixed): mixed,
93
+ // The events a writer has to listen to rather than assume: `drain`, so a body
94
+ // is paced by what the socket will take, and `close`, so a client that hung
95
+ // up stops the producer instead of being written at. Named individually, like
96
+ // `stream.js`'s `NodeDestination`, so that a host missing one of them fails
97
+ // to compile rather than to serve.
98
+ on(event: string, listener: (...args: Array<mixed>) => mixed): mixed,
99
+ once(event: string, listener: (...args: Array<mixed>) => mixed): mixed,
100
+ off(event: string, listener: (...args: Array<mixed>) => mixed): mixed,
101
+ ...
102
+ };
103
+
104
+ /** One file from `dist/`, as `uf build --compile` embedded it. */
105
+ export type EmbeddedAsset = {|
106
+ /** The `content-type` to serve it with, decided at build time. */
107
+ readonly type: string,
108
+ /** The file's bytes, base64. */
109
+ readonly body: string,
110
+ |};
111
+
112
+ /** Every embedded file, keyed by its path relative to the output directory. */
113
+ export type EmbeddedAssets = { readonly [path: string]: EmbeddedAsset };
114
+
115
+ /** The script, stylesheet and preload URLs a rendered document references. */
116
+ export type DocumentAssets = {|
117
+ readonly scripts: $ReadOnlyArray<string>,
118
+ readonly styles: $ReadOnlyArray<string>,
119
+ readonly preloads: $ReadOnlyArray<string>,
120
+ |};
121
+
122
+ /** What the project's server bundle exports; see `virtual:uf/server`. */
123
+ export type StandaloneApp = {|
124
+ /**
125
+ * Render `url`, resolving when the *shell* is ready.
126
+ *
127
+ * The same `{ status, headers?, pipe }` the router hands `uf start` and
128
+ * every adapter — not a finished string. A binary that collected the whole
129
+ * document before answering would be the one deployment target that does not
130
+ * stream, and the reason `renderToString` was replaced is that the wait is
131
+ * the slowest thing on the page.
132
+ */
133
+ readonly render: (
134
+ url: string,
135
+ assets: DocumentAssets,
136
+ options?: {| readonly onError?: (error: mixed) => void |},
137
+ ) => Promise<{|
138
+ readonly status: number,
139
+ readonly headers?: { readonly [string]: string },
140
+ // A promise, and not `void`: `DocumentBody.pipe` resolves on the last byte
141
+ // and rejects when the render fails after the shell. Typing it away was
142
+ // how the rejection below came to be dropped.
143
+ readonly pipe: (destination: NodeResponse) => Promise<void>,
144
+ readonly stream: () => ReadableStream<Uint8Array>,
145
+ |}>,
146
+ readonly dispatch: (request: Request) => Promise<Response | null>,
147
+ /**
148
+ * The guard on the path, run before anything under it answers.
149
+ *
150
+ * Called rather than tested for: a server bundle without it is a `TypeError`
151
+ * on the first request, not an application whose auth check quietly stopped
152
+ * running once it was compiled. See ubugeeei-prod/uf#260, and
153
+ * `@uniflowed/vite`'s `createApplicationHandler`, which says the same thing
154
+ * about `uf preview` and `uf start`.
155
+ */
156
+ readonly runMiddleware: (request: Request) => Promise<Response | null>,
157
+ /**
158
+ * Begin the request everything above runs inside.
159
+ *
160
+ * From the application bundle rather than from this module's own import of
161
+ * `@uniflowed/server/host`, and that is not a stylistic choice: the request
162
+ * lives in an `AsyncLocalStorage` belonging to one module instance, and the
163
+ * instance that matters is the one the bundled router, middleware and pages
164
+ * resolved to. Beginning a request in a second storage would leave every
165
+ * `cookies()` in the application outside one, silently.
166
+ *
167
+ * `run` wraps everything that decides the response; `settle` is called after
168
+ * the last byte, which here is after `send`, after `sendBytes`, and after
169
+ * `pipe` resolves. See ubugeeei-prod/uf#389.
170
+ */
171
+ readonly beginRequest: (request: Request) => {|
172
+ readonly run: <T>(body: () => Promise<T>) => Promise<T>,
173
+ readonly settle: () => Promise<void>,
174
+ |},
175
+ |};
176
+
177
+ /** Everything an application needs to answer a request, all of it built in. */
178
+ export type HandlerOptions = {|
179
+ readonly app: StandaloneApp,
180
+ readonly assets: EmbeddedAssets,
181
+ readonly document: DocumentAssets,
182
+ |};
183
+
184
+ /** What [`serve`] needs: the above, and where to listen. */
185
+ export type ServeOptions = {|
186
+ readonly app: StandaloneApp,
187
+ readonly assets: EmbeddedAssets,
188
+ readonly document: DocumentAssets,
189
+ /** Overridden by `--port` and then by `PORT`; defaults to 3000. */
190
+ readonly port?: number,
191
+ /** Overridden by `--host` and then by `HOST`; defaults to loopback. */
192
+ readonly host?: string,
193
+ |};
194
+
195
+ /**
196
+ * How long a browser may keep a file that is not a document.
197
+ *
198
+ * One hour, uniformly, and deliberately not the year-long `immutable` a
199
+ * content-hashed chunk could take: this module cannot tell a hashed chunk from
200
+ * an unhashed file copied out of `public/`, because `dist/` records no such
201
+ * distinction, and getting it wrong in the `immutable` direction pins a stale
202
+ * favicon in every visitor's cache with no way to recall it. A binary behind a
203
+ * CDN should let the CDN decide; an hour is the safe answer for one that is not.
204
+ */
205
+ const ASSET_CACHE_CONTROL = "public, max-age=3600";
206
+
207
+ /** Documents are revalidated every time, because a deploy replaces them. */
208
+ const DOCUMENT_CACHE_CONTROL = "no-cache";
209
+
210
+ /**
211
+ * Serve the application until the process is stopped.
212
+ *
213
+ * Resolves once the socket is listening, with the address it took — a caller
214
+ * that asked for port 0 has no other way to learn which port it got, and the
215
+ * test that drives a compiled binary needs exactly that.
216
+ */
217
+ export async function serve(options: ServeOptions): Promise<{|
218
+ readonly host: string,
219
+ readonly port: number,
220
+ readonly close: () => Promise<void>,
221
+ |}> {
222
+ const handle = createHandler(options);
223
+
224
+ // Said before the socket, not after it, and that ordering is the point. It
225
+ // is the one fact about a compiled binary that cannot be checked from
226
+ // outside it — whether `dist/` really came along — and a count printed only
227
+ // on a successful bind is a count nobody can see on a machine that is not
228
+ // allowed to bind. `compile_writes_one_file_that_carries_the_whole_site`
229
+ // reads this line and nothing else.
230
+ process.stdout.write(`uf: ${String(Object.keys(options.assets).length)} embedded files\n`);
231
+
232
+ const server = createServer((request: NodeRequest, response: NodeResponse) => {
233
+ handle(request, response).catch((error: mixed) => {
234
+ // A request that throws is this server's last chance to say so: there is
235
+ // no framework above it and no log drain beside it. Report it on stderr
236
+ // and answer 500, rather than letting the host's unhandled-rejection
237
+ // policy decide whether the process survives.
238
+ process.stderr.write(`uf: ${String(error?.stack ?? error)}\n`);
239
+ try {
240
+ response.statusCode = 500;
241
+ response.setHeader("content-type", "text/plain; charset=utf-8");
242
+ response.end("internal server error\n");
243
+ } catch {
244
+ // The response was already partly written; nothing left to say.
245
+ }
246
+ });
247
+ });
248
+
249
+ const host = options.host ?? argument("--host") ?? process.env.HOST ?? "127.0.0.1";
250
+ const port = options.port ?? Number(argument("--port") ?? process.env.PORT ?? 3000);
251
+
252
+ await new Promise((resolve, reject) => {
253
+ server.once("error", reject);
254
+ server.listen(port, host, resolve);
255
+ });
256
+
257
+ const address = server.address();
258
+ const bound = typeof address === "object" && address != null ? address.port : port;
259
+ process.stdout.write(`uf: listening on http://${host}:${String(bound)}\n`);
260
+
261
+ return {
262
+ host,
263
+ port: bound,
264
+ close: () =>
265
+ new Promise((resolve) => {
266
+ server.close(() => resolve());
267
+ }),
268
+ };
269
+ }
270
+
271
+ /**
272
+ * The embedded files, as a map.
273
+ *
274
+ * A `Map` rather than the generated object itself, because a lookup keyed by a
275
+ * request path must not be able to find `__proto__` or `constructor`. Building
276
+ * it costs one pass over a few hundred entries at startup and removes the
277
+ * question entirely.
278
+ *
279
+ * The bytes are decoded lazily and then kept: a build that embeds a hundred
280
+ * megabytes of sourcemaps should not spend the startup decoding the ones this
281
+ * process will never be asked for.
282
+ */
283
+ function index(assets: EmbeddedAssets): Map<string, {| +type: string, +bytes: () => Buffer |}> {
284
+ const files = new Map();
285
+ for (const path of Object.keys(assets)) {
286
+ const asset = assets[path];
287
+ let decoded: Buffer | null = null;
288
+ files.set(path, {
289
+ type: asset.type,
290
+ bytes: () => {
291
+ if (decoded == null) {
292
+ // `Buffer.from` rather than `atob`: `atob` answers with a string of
293
+ // char codes, and turning megabytes of that into bytes is a loop in
294
+ // JavaScript. Every host that has `node:http` has `node:buffer`.
295
+ decoded = Buffer.from(asset.body, "base64");
296
+ }
297
+ return decoded;
298
+ },
299
+ });
300
+ }
301
+ return files;
302
+ }
303
+
304
+ /**
305
+ * One request, answered — exported so an application can be mounted rather
306
+ * than only run.
307
+ *
308
+ * `serve` is the whole of a compiled binary, and it is not the whole of what
309
+ * anybody wants: a uf application behind an existing Node server, or beside
310
+ * other routes in one process, needs the request handling without the socket.
311
+ * That is this. It is also what the tests drive, which is not a coincidence —
312
+ * a request handler that can only be reached through a listening socket is one
313
+ * that can only be tested on a machine allowed to bind one.
314
+ *
315
+ * The order is `internal/serve.js`'s, which is Vite's:
316
+ *
317
+ * 1. a file `uf build` already wrote, for `GET` and `HEAD` only — an
318
+ * embedded path that matches exactly, such as `/assets/index-a1b2c3.js`
319
+ * or anything copied out of `public/`, and then the prerendered document
320
+ * for this URL, because `/guide` was written as `guide/index.html`;
321
+ * 2. a route handler, for any method, because a handler is the only thing
322
+ * that answers a `POST` and may also answer a `GET` for a path with no
323
+ * page;
324
+ * 3. and otherwise the renderer, which also produces the 404.
325
+ *
326
+ * The prerendered document is looked up *before* the dispatcher, and that is
327
+ * the one place this used to disagree with `uf preview` and `uf start`. The
328
+ * router allows a handler to sit beside a page in the same directory, so a
329
+ * path can have both — and Vite's preview server serves the file first with no
330
+ * say in the matter, so a binary that let the handler win would answer
331
+ * differently from the command a build is checked with. Answering the same
332
+ * wrong-looking way as the other two is worth more than answering a better way
333
+ * alone.
334
+ *
335
+ * A page never answers a `POST`: letting one try turns a missing handler into
336
+ * a rendered page with a 200 where the caller expected a 405.
337
+ */
338
+ export function createHandler(
339
+ options: HandlerOptions,
340
+ ): (NodeRequest, NodeResponse) => Promise<void> {
341
+ const { app, document } = options;
342
+ const files = index(options.assets);
343
+
344
+ return async function handle(request: NodeRequest, response: NodeResponse): Promise<void> {
345
+ const method = (request.method ?? "GET").toUpperCase();
346
+ const url = new URL(request.url ?? "/", "http://localhost");
347
+ const pathname = decodePath(url.pathname);
348
+
349
+ if (pathname != null && (method === "GET" || method === "HEAD")) {
350
+ const file = files.get(assetKey(pathname));
351
+ if (file != null) {
352
+ sendBytes(response, method, 200, file.type, ASSET_CACHE_CONTROL, file.bytes());
353
+ return;
354
+ }
355
+ // A document is revalidated where an asset is cached, because a deploy
356
+ // replaces documents and gives assets a new hashed name.
357
+ const page = files.get(documentKey(pathname));
358
+ if (page != null) {
359
+ sendBytes(response, method, 200, page.type, DOCUMENT_CACHE_CONTROL, page.bytes());
360
+ return;
361
+ }
362
+ }
363
+
364
+ // The request begins here rather than at the top of the handler, and the
365
+ // two lookups above are why: an embedded chunk and a prerendered document
366
+ // are answered without any application code running at all, so there is
367
+ // nothing that could ask for cookies and nothing that could defer work.
368
+ // What is below is the application, and it is what a request is for.
369
+ //
370
+ // `app.beginRequest` and not this module's own import: the storage that
371
+ // holds a request belongs to one copy of `@uniflowed/server`, and the copy
372
+ // that matters is the one linked into the bundle beside this file.
373
+ //
374
+ // `settle` is in a `finally` and it is the last thing the handler does, so
375
+ // every `after()` runs after the response has been written — after `send`,
376
+ // after `sendBytes`, and after `pipe` resolves — which is what `after()`
377
+ // promises and what the other three hosts do. A request that failed is
378
+ // still a request that happened, so the drain is owed either way; see
379
+ // ubugeeei-prod/uf#389.
380
+ const asRequest = toRequest(request, url);
381
+ const { run, settle } = app.beginRequest(asRequest);
382
+ try {
383
+ await run(async () => {
384
+ // Middleware above the dispatcher and above the render, and below the two
385
+ // lookups on purpose. It guards a path, so it must run for a page, for a
386
+ // route handler, and for a path under it that matches neither — but an
387
+ // embedded asset and a prerendered document are answered before it, which
388
+ // is exactly what `uf preview` does, because Vite's file middleware runs
389
+ // before anything mounted behind it. The three front doors have to give
390
+ // one answer; that a prerendered page under a guard ships unguarded is
391
+ // true of all of them and is ubugeeei-prod/uf#342.
392
+ const guarded = await app.runMiddleware(asRequest);
393
+ if (guarded != null) {
394
+ await sendUnlessHead(response, method, guarded);
395
+ return;
396
+ }
397
+
398
+ const handled = await app.dispatch(asRequest);
399
+ if (handled != null) {
400
+ await sendUnlessHead(response, method, handled);
401
+ return;
402
+ }
403
+
404
+ if (method !== "GET" && method !== "HEAD") {
405
+ // A page supports exactly `GET` and `HEAD`, which is why the `Allow` the
406
+ // specification requires on every 405 can be written here even though
407
+ // this side of the handler knows nothing about methods. A *handler* path
408
+ // with the wrong method never reaches this line: the dispatcher answers
409
+ // that one itself, with the methods that module really exports.
410
+ response.setHeader("allow", "GET, HEAD");
411
+ sendBytes(
412
+ response,
413
+ method,
414
+ 405,
415
+ "text/plain; charset=utf-8",
416
+ DOCUMENT_CACHE_CONTROL,
417
+ Buffer.from("method not allowed\n"),
418
+ );
419
+ return;
420
+ }
421
+
422
+ const rendered = await app.render(url.pathname + url.search, document, {
423
+ // There is no terminal to render into: this is a binary somebody started
424
+ // with `./app`, possibly under a supervisor. The console is where a
425
+ // supervisor looks, and losing a boundary's exception entirely would be
426
+ // worse — it is the only trace a page that failed after its first byte
427
+ // leaves anywhere.
428
+ onError: (error) => {
429
+ console.error(error);
430
+ },
431
+ });
432
+ response.statusCode = rendered.status;
433
+ response.setHeader("content-type", "text/html; charset=utf-8");
434
+ response.setHeader("cache-control", DOCUMENT_CACHE_CONTROL);
435
+ for (const name of Object.keys(rendered.headers ?? {})) {
436
+ response.setHeader(name, (rendered.headers ?? {})[name]);
437
+ }
438
+ // No `content-length`: the length is not known until the last byte, and
439
+ // waiting for it is the whole of what streaming is not. `HEAD` gets the
440
+ // status and the headers, and the stream is cancelled rather than dropped
441
+ // so the render behind it stops instead of filling its queue and waiting
442
+ // for a reader that is never coming.
443
+ if (method === "HEAD") {
444
+ await rendered.stream().cancel();
445
+ response.end();
446
+ return;
447
+ }
448
+ // Awaited, because `pipe` rejects: React hands a post-shell failure to the
449
+ // destination's `destroy(error)`, `ChunkQueue.fail` records it, and the
450
+ // generator `pipe` is iterating rethrows it. Called and dropped, that
451
+ // rejection escapes this handler — `serve`'s `handle(…).catch` has already
452
+ // resolved — and lands on the process, where `--unhandled-rejections=throw`
453
+ // is the default and a binary someone started with `./app` exits in the
454
+ // middle of a request that was otherwise recoverable.
455
+ //
456
+ // It cannot become a 500. The shell went out with its status and headers
457
+ // long before this, and `pipe`'s own `finally` has already called `end()`.
458
+ // What is left is to say so where a supervisor looks, and to drop the
459
+ // socket: a chunked response that is closed cleanly is a client being told
460
+ // a truncated document is the whole document, which is the failure this
461
+ // pull request is named after.
462
+ try {
463
+ await rendered.pipe(response);
464
+ } catch (error) {
465
+ process.stderr.write(`uf: ${String(error?.stack ?? error)}\n`);
466
+ response.destroy(error);
467
+ }
468
+ });
469
+ } finally {
470
+ await settle();
471
+ }
472
+ };
473
+ }
474
+
475
+ /**
476
+ * A request path as an embedded key, or `null` when it cannot be one.
477
+ *
478
+ * Percent-decoding happens here rather than at the lookup, because a path that
479
+ * does not decode is a malformed request and not a missing file.
480
+ */
481
+ function decodePath(pathname: string): string | null {
482
+ try {
483
+ return decodeURIComponent(pathname);
484
+ } catch {
485
+ return null;
486
+ }
487
+ }
488
+
489
+ /** `/assets/x.js` is the embedded `assets/x.js`. */
490
+ function assetKey(pathname: string): string {
491
+ return pathname.replace(/^\/+/, "");
492
+ }
493
+
494
+ /**
495
+ * The prerendered document for a URL.
496
+ *
497
+ * `uf build` writes `/guide` as `guide/index.html`, and `/` as `index.html`,
498
+ * so both spellings of a directory URL find the same file.
499
+ */
500
+ function documentKey(pathname: string): string {
501
+ const key = assetKey(pathname).replace(/\/+$/, "");
502
+ return key === "" ? "index.html" : `${key}/index.html`;
503
+ }
504
+
505
+ /**
506
+ * A Node request as a `Request`.
507
+ *
508
+ * The body is passed as a stream where the host allows it, so a handler that
509
+ * accepts an upload does not need the whole thing buffered before it starts.
510
+ * `duplex` is required by the specification whenever a body is a stream, and
511
+ * Node throws without it.
512
+ */
513
+ function toRequest(incoming: NodeRequest, url: URL): Request {
514
+ const headers = new Headers();
515
+ for (const name of Object.keys(incoming.headers)) {
516
+ const value = incoming.headers[name];
517
+ if (value == null) continue;
518
+ for (const entry of Array.isArray(value) ? value : [value]) {
519
+ headers.append(name, entry);
520
+ }
521
+ }
522
+
523
+ const host = headers.get("host");
524
+ if (host != null && host !== "") {
525
+ url.host = host;
526
+ }
527
+
528
+ const method = (incoming.method ?? "GET").toUpperCase();
529
+ const init: { [string]: mixed } = { method, headers };
530
+ if (method !== "GET" && method !== "HEAD") {
531
+ init.body = incoming;
532
+ init.duplex = "half";
533
+ }
534
+ // $FlowFixMe[incompatible-call] - `incoming` is a stream, which `Request` accepts.
535
+ return new Request(url, init);
536
+ }
537
+
538
+ /** `send`, except that a `HEAD` gets the status and the headers and no body. */
539
+ async function sendUnlessHead(
540
+ outgoing: NodeResponse,
541
+ method: string,
542
+ result: Response,
543
+ ): Promise<void> {
544
+ if (method === "HEAD") {
545
+ outgoing.statusCode = result.status;
546
+ for (const [name, value] of result.headers) {
547
+ outgoing.setHeader(name, value);
548
+ }
549
+ outgoing.end();
550
+ return;
551
+ }
552
+ await send(outgoing, result);
553
+ }
554
+
555
+ /**
556
+ * Write a `Response` to a Node response, minding the socket.
557
+ *
558
+ * `send` was written a third time here, with a comment saying the three copies
559
+ * had to answer alike because "a binary that buffered where `uf start` paced
560
+ * would be the one deployment target whose memory profile nobody had
561
+ * measured". They did not stay alike — ubugeeei-prod/uf#400 is the copy in
562
+ * `@uniflowed/server`'s `node.js` losing the pacing while this one kept it.
563
+ *
564
+ * The reason not to share was that importing `@uniflowed/vite` into the
565
+ * artefact a deployment runs is the property `uf start` exists to establish.
566
+ * That reason is gone: the loop lives in `@uniflowed/server` now, which is the
567
+ * package this file is *in*.
568
+ *
569
+ * `HEAD` stays here, at the call site, because it is a decision about a
570
+ * request rather than about writing a body.
571
+ */
572
+
573
+ /** Write one embedded file, with the length a client needs to reuse a socket. */
574
+ function sendBytes(
575
+ outgoing: NodeResponse,
576
+ method: string,
577
+ status: number,
578
+ type: string,
579
+ cacheControl: string,
580
+ bytes: Buffer,
581
+ ): void {
582
+ outgoing.statusCode = status;
583
+ outgoing.setHeader("content-type", type);
584
+ outgoing.setHeader("cache-control", cacheControl);
585
+ outgoing.setHeader("content-length", String(bytes.byteLength));
586
+ outgoing.end(method === "HEAD" ? undefined : bytes);
587
+ }
588
+
589
+ /** The value of a `--flag value` pair on the command line, if it is there. */
590
+ function argument(name: string): string | null {
591
+ const at = process.argv.indexOf(name);
592
+ return at === -1 ? null : (process.argv[at + 1] ?? null);
593
+ }