@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/cache.js +341 -0
- package/edge.js +158 -0
- package/fetch.js +310 -0
- package/host.js +56 -0
- package/index.js +133 -0
- package/internal/application.js +90 -0
- package/internal/cache-key.js +79 -0
- package/internal/cache-store.js +524 -0
- package/internal/context.js +285 -0
- package/lambda.js +266 -0
- package/node.js +460 -0
- package/package.json +34 -0
- package/standalone.js +593 -0
package/node.js
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
//
|
|
3
|
+
// `@uniflowed/server/node`: the Node half of serving a build.
|
|
4
|
+
//
|
|
5
|
+
// [`../fetch.js`] answers the application's half of a request and touches no
|
|
6
|
+
// filesystem, because that is the half a worker runs. This is everything that
|
|
7
|
+
// is left, and all of it is host-specific: reading a file out of a directory,
|
|
8
|
+
// translating between Node's request objects and the platform's, and taking a
|
|
9
|
+
// socket.
|
|
10
|
+
//
|
|
11
|
+
// Three things use it, and the point of it being one module is that they
|
|
12
|
+
// cannot answer differently:
|
|
13
|
+
//
|
|
14
|
+
// * `uf start`, through `@uniflowed/vite`'s `internal/serve.js`;
|
|
15
|
+
// * `uf preview`, through the same;
|
|
16
|
+
// * the `server.js` that `uf build --adapter node` writes, which imports
|
|
17
|
+
// this directly — a deployed application must not link the package named
|
|
18
|
+
// after the bundler, and before this module existed the only copy of the
|
|
19
|
+
// static half was inside `@uniflowed/vite`.
|
|
20
|
+
//
|
|
21
|
+
// The exception is `./standalone.js`, which serves from bytes it carries
|
|
22
|
+
// instead of from a directory; its header says why it is a second
|
|
23
|
+
// implementation rather than a caller of this one.
|
|
24
|
+
//
|
|
25
|
+
// # Who owns the request
|
|
26
|
+
//
|
|
27
|
+
// This module does, for every host that reaches it: [`nodeListener`] begins
|
|
28
|
+
// the request, runs the whole of answering it inside `run`, and settles it on
|
|
29
|
+
// the line after the last byte — after its own 500, when it wrote one. That is
|
|
30
|
+
// what `after()` promises and it is not something a `Request` → `Response`
|
|
31
|
+
// handler can promise for itself, because such a handler has a `Response` in
|
|
32
|
+
// hand and not a response on the wire.
|
|
33
|
+
//
|
|
34
|
+
// `beginRequest` is passed in rather than imported from `./internal/context.js`
|
|
35
|
+
// beside this file, and that is the one thing about this module that looks
|
|
36
|
+
// wrong and is not. The request lives in an `AsyncLocalStorage` belonging to a
|
|
37
|
+
// module *instance*, and the instance the application reads is the one bundled
|
|
38
|
+
// into `.uf/build/server/server.js` — not the one this file resolves from the
|
|
39
|
+
// host's `node_modules`. A host that began a request in the wrong storage
|
|
40
|
+
// would fail silently: the guard would run, the page would render, and every
|
|
41
|
+
// `cookies()` in it would throw as though no host had run at all. So the
|
|
42
|
+
// bundle hands it out, `@uniflowed/router/server` re-exports it, and a caller
|
|
43
|
+
// passes it here. See ubugeeei-prod/uf#389.
|
|
44
|
+
//
|
|
45
|
+
// # The order is Vite's
|
|
46
|
+
//
|
|
47
|
+
// Static files first, then the application. That is a compatibility
|
|
48
|
+
// requirement rather than a preference: Vite's preview server runs its own
|
|
49
|
+
// file middleware before anything mounted behind it can see a request, so
|
|
50
|
+
// `uf preview` serves a file first whether or not anything here agrees — and a
|
|
51
|
+
// deployment that disagreed would mean a project whose handler path collides
|
|
52
|
+
// with a file in `public/` behaves one way when it is checked and the other
|
|
53
|
+
// way when it is deployed.
|
|
54
|
+
|
|
55
|
+
import { createReadStream } from "node:fs";
|
|
56
|
+
import { stat } from "node:fs/promises";
|
|
57
|
+
import { createServer } from "node:http";
|
|
58
|
+
import path from "node:path";
|
|
59
|
+
import { Readable } from "node:stream";
|
|
60
|
+
|
|
61
|
+
import type { RequestLifecycle } from "./internal/context.js";
|
|
62
|
+
|
|
63
|
+
export type { RequestLifecycle } from "./internal/context.js";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Content types for what a uf build emits.
|
|
67
|
+
*
|
|
68
|
+
* A closed table rather than a dependency, and deliberately short: every entry
|
|
69
|
+
* is an extension `uf build` actually writes or a project actually puts in
|
|
70
|
+
* `public/`. Anything else is `application/octet-stream`, which a browser
|
|
71
|
+
* downloads rather than executes — the safe answer for a file whose type we do
|
|
72
|
+
* not know, and the reason this is not a guess based on the bytes.
|
|
73
|
+
*/
|
|
74
|
+
const CONTENT_TYPES: { readonly [string]: string } = Object.freeze({
|
|
75
|
+
".avif": "image/avif",
|
|
76
|
+
".css": "text/css; charset=utf-8",
|
|
77
|
+
".gif": "image/gif",
|
|
78
|
+
".html": "text/html; charset=utf-8",
|
|
79
|
+
".ico": "image/x-icon",
|
|
80
|
+
".jpeg": "image/jpeg",
|
|
81
|
+
".jpg": "image/jpeg",
|
|
82
|
+
".js": "text/javascript; charset=utf-8",
|
|
83
|
+
".json": "application/json; charset=utf-8",
|
|
84
|
+
".map": "application/json; charset=utf-8",
|
|
85
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
86
|
+
".png": "image/png",
|
|
87
|
+
".svg": "image/svg+xml",
|
|
88
|
+
".txt": "text/plain; charset=utf-8",
|
|
89
|
+
".webmanifest": "application/manifest+json",
|
|
90
|
+
".webp": "image/webp",
|
|
91
|
+
".woff": "font/woff",
|
|
92
|
+
".woff2": "font/woff2",
|
|
93
|
+
".xml": "application/xml; charset=utf-8",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The pieces of a Node request this module touches.
|
|
98
|
+
*
|
|
99
|
+
* Declared structurally rather than imported from a `node:http` libdef, for
|
|
100
|
+
* the same reason `./standalone.js` does it: the set is small, and naming it
|
|
101
|
+
* here is what lets the file be read without knowing which host's types are in
|
|
102
|
+
* scope. `originalUrl` is Connect's, and it is here because `uf preview` runs
|
|
103
|
+
* this behind Vite's middleware stack, which sets it.
|
|
104
|
+
*/
|
|
105
|
+
export type NodeRequest = {
|
|
106
|
+
readonly method?: string,
|
|
107
|
+
readonly url?: string,
|
|
108
|
+
readonly originalUrl?: string,
|
|
109
|
+
readonly headers: { readonly [string]: string | Array<string> | void },
|
|
110
|
+
...
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
/** The pieces of a Node response this module writes. */
|
|
114
|
+
export type NodeResponse = {
|
|
115
|
+
statusCode: number,
|
|
116
|
+
statusMessage: string,
|
|
117
|
+
headersSent: boolean,
|
|
118
|
+
setHeader(name: string, value: string): mixed,
|
|
119
|
+
write(chunk: Uint8Array | string): boolean,
|
|
120
|
+
end(chunk?: Uint8Array | string): mixed,
|
|
121
|
+
destroy(error?: mixed): mixed,
|
|
122
|
+
// `send` paces itself against the socket and stops when the client hangs
|
|
123
|
+
// up, so it needs the events as well as the writes. `write` returns a
|
|
124
|
+
// `boolean` for the same reason — `mixed` would have made the back-pressure
|
|
125
|
+
// check unwritable, which is one way this was lost.
|
|
126
|
+
on(event: string, listener: () => mixed): mixed,
|
|
127
|
+
once(event: string, listener: () => mixed): mixed,
|
|
128
|
+
off(event: string, listener: () => mixed): mixed,
|
|
129
|
+
...
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* A Node request as a `Request`.
|
|
134
|
+
*
|
|
135
|
+
* The body is passed as a stream where the host allows it, so a handler that
|
|
136
|
+
* accepts an upload does not need the whole thing buffered before it starts.
|
|
137
|
+
* `duplex` is required by the specification whenever a body is a stream, and
|
|
138
|
+
* Node throws without it.
|
|
139
|
+
*/
|
|
140
|
+
export function toRequest(
|
|
141
|
+
incoming: NodeRequest,
|
|
142
|
+
options?: {| readonly secure?: boolean |},
|
|
143
|
+
): Request {
|
|
144
|
+
const host = incoming.headers.host;
|
|
145
|
+
const authority = typeof host === "string" && host !== "" ? host : "localhost";
|
|
146
|
+
const protocol = options?.secure === true ? "https" : "http";
|
|
147
|
+
const url = new URL(incoming.originalUrl ?? incoming.url ?? "/", `${protocol}://${authority}`);
|
|
148
|
+
|
|
149
|
+
const headers = new Headers();
|
|
150
|
+
for (const name of Object.keys(incoming.headers)) {
|
|
151
|
+
const value = incoming.headers[name];
|
|
152
|
+
if (value == null) continue;
|
|
153
|
+
for (const entry of Array.isArray(value) ? value : [value]) {
|
|
154
|
+
headers.append(name, entry);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const method = (incoming.method ?? "GET").toUpperCase();
|
|
159
|
+
const init: { [string]: mixed } = { method, headers };
|
|
160
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
161
|
+
init.body = incoming;
|
|
162
|
+
init.duplex = "half";
|
|
163
|
+
}
|
|
164
|
+
// $FlowFixMe[incompatible-call] - `incoming` is a stream, which `Request` accepts.
|
|
165
|
+
return new Request(url, init);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Write a `Response` to a Node response. */
|
|
169
|
+
export async function send(outgoing: NodeResponse, result: Response): Promise<void> {
|
|
170
|
+
outgoing.statusCode = result.status;
|
|
171
|
+
if (result.statusText !== "") {
|
|
172
|
+
outgoing.statusMessage = result.statusText;
|
|
173
|
+
}
|
|
174
|
+
for (const [name, value] of result.headers) {
|
|
175
|
+
outgoing.setHeader(name, value);
|
|
176
|
+
}
|
|
177
|
+
if (result.body == null) {
|
|
178
|
+
outgoing.end();
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// Streamed rather than buffered, so a handler returning a large or
|
|
182
|
+
// open-ended body is not read into memory first.
|
|
183
|
+
//
|
|
184
|
+
// Which was only half true while this loop read as fast as the body would
|
|
185
|
+
// give: `write` answers `false` when the kernel buffer is full and the rest
|
|
186
|
+
// is being held in *this process's* memory, and a reader that ignores that
|
|
187
|
+
// turns a slow client into a heap the size of everything it has not
|
|
188
|
+
// acknowledged. Streaming in shape and buffering in fact — the same failure
|
|
189
|
+
// `ChunkQueue` exists to avoid a layer up, in the renderer.
|
|
190
|
+
const reader = result.body.getReader();
|
|
191
|
+
// And a client that hangs up is the other half. Nothing written after that
|
|
192
|
+
// goes anywhere, and the producer behind the body — a render, a proxied
|
|
193
|
+
// upstream, an event stream — keeps producing for a reader that is never
|
|
194
|
+
// coming back. `cancel()` is what tells it to stop; `releaseLock()` would
|
|
195
|
+
// only detach this end.
|
|
196
|
+
let open = true;
|
|
197
|
+
const onClose = () => {
|
|
198
|
+
open = false;
|
|
199
|
+
};
|
|
200
|
+
outgoing.on("close", onClose);
|
|
201
|
+
try {
|
|
202
|
+
while (open) {
|
|
203
|
+
const { done, value } = await reader.read();
|
|
204
|
+
if (done === true || !open) break;
|
|
205
|
+
if (value != null && outgoing.write(value) === false) {
|
|
206
|
+
await writable(outgoing);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} finally {
|
|
210
|
+
outgoing.off("close", onClose);
|
|
211
|
+
}
|
|
212
|
+
if (open) {
|
|
213
|
+
outgoing.end();
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// Best effort, and the only place in this function where a rejection is
|
|
217
|
+
// dropped: the connection is already gone, so there is nobody left to report
|
|
218
|
+
// to and no response left to fail.
|
|
219
|
+
await reader.cancel().catch(() => {});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Resolve once `outgoing` can take more — or once it cannot ever again.
|
|
224
|
+
*
|
|
225
|
+
* `drain` alone would be a deadlock waiting to happen: a client that hangs up
|
|
226
|
+
* while the buffer is full emits `close` and never `drain`, and a writer
|
|
227
|
+
* waiting only for the latter waits for the life of the process, holding the
|
|
228
|
+
* body's producer open with it.
|
|
229
|
+
*/
|
|
230
|
+
function writable(outgoing: NodeResponse): Promise<void> {
|
|
231
|
+
return new Promise((resolve) => {
|
|
232
|
+
const settle = () => {
|
|
233
|
+
outgoing.off("drain", settle);
|
|
234
|
+
outgoing.off("close", settle);
|
|
235
|
+
resolve();
|
|
236
|
+
};
|
|
237
|
+
outgoing.once("drain", settle);
|
|
238
|
+
outgoing.once("close", settle);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The static half: a file under `root`, or `null` for the caller to carry on.
|
|
244
|
+
*
|
|
245
|
+
* `GET` and `HEAD` only. A `POST` to a path that happens to have a file under
|
|
246
|
+
* it belongs to a route handler, and answering it with the file's bytes would
|
|
247
|
+
* be the same mistake as rendering a page for it.
|
|
248
|
+
*
|
|
249
|
+
* # The path is checked once, after it is resolved
|
|
250
|
+
*
|
|
251
|
+
* `docs/security.md` rule 2: never authorize against a raw request string or a
|
|
252
|
+
* partially decoded path. The pathname is decoded first, then resolved against
|
|
253
|
+
* the root, and *then* checked to be inside it — so `%2e%2e%2f`, a backslash
|
|
254
|
+
* on Windows, and a symlinked directory all reduce to the same question, asked
|
|
255
|
+
* once, of the value that is actually opened.
|
|
256
|
+
*/
|
|
257
|
+
export function createStaticHandler(options: {|
|
|
258
|
+
readonly root: string,
|
|
259
|
+
|}): (request: Request) => Promise<Response | null> {
|
|
260
|
+
const rootDir = path.resolve(options.root);
|
|
261
|
+
|
|
262
|
+
return async function serveStatic(request: Request): Promise<Response | null> {
|
|
263
|
+
const method = request.method.toUpperCase();
|
|
264
|
+
if (method !== "GET" && method !== "HEAD") return null;
|
|
265
|
+
|
|
266
|
+
const pathname = decodePathname(new URL(request.url).pathname);
|
|
267
|
+
if (pathname == null) return null;
|
|
268
|
+
|
|
269
|
+
const resolved = path.resolve(rootDir, `.${pathname}`);
|
|
270
|
+
if (resolved !== rootDir && !resolved.startsWith(rootDir + path.sep)) return null;
|
|
271
|
+
|
|
272
|
+
// `/guide/` and `/guide` are the same prerendered document, and neither
|
|
273
|
+
// spelling is the one a person types. `<path>.html` is last because a
|
|
274
|
+
// build writes `guide/index.html`, and only a hand-placed file in
|
|
275
|
+
// `public/` is ever `guide.html`.
|
|
276
|
+
const candidates =
|
|
277
|
+
pathname.endsWith("/") === true
|
|
278
|
+
? [path.join(resolved, "index.html")]
|
|
279
|
+
: [resolved, path.join(resolved, "index.html"), `${resolved}.html`];
|
|
280
|
+
|
|
281
|
+
for (const candidate of candidates) {
|
|
282
|
+
const info = await statFile(candidate);
|
|
283
|
+
if (info == null || !info.isFile()) continue;
|
|
284
|
+
const headers = {
|
|
285
|
+
"content-type":
|
|
286
|
+
CONTENT_TYPES[path.extname(candidate).toLowerCase()] ?? "application/octet-stream",
|
|
287
|
+
"content-length": String(info.size),
|
|
288
|
+
};
|
|
289
|
+
if (method === "HEAD") return new Response(null, { headers });
|
|
290
|
+
// Streamed rather than read into memory, so serving a large asset costs
|
|
291
|
+
// a buffer rather than the file.
|
|
292
|
+
// $FlowFixMe[incompatible-call] - a Node web stream is a `BodyInit`.
|
|
293
|
+
return new Response(Readable.toWeb(createReadStream(candidate)), { headers });
|
|
294
|
+
}
|
|
295
|
+
return null;
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function decodePathname(pathname: string): string | null {
|
|
300
|
+
try {
|
|
301
|
+
const decoded = decodeURIComponent(pathname);
|
|
302
|
+
// A NUL truncates the name every C-level `open` sees, so a path holding
|
|
303
|
+
// one is refused rather than normalised into something shorter.
|
|
304
|
+
return decoded.includes("\0") ? null : decoded;
|
|
305
|
+
} catch {
|
|
306
|
+
// A percent escape that is not one. There is no file behind it.
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function statFile(file: string) {
|
|
312
|
+
try {
|
|
313
|
+
return await stat(file);
|
|
314
|
+
} catch {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* A `Request`/`Response` handler as a Node request listener.
|
|
321
|
+
*
|
|
322
|
+
* The handler contract is the platform's, so this adapter belongs here rather
|
|
323
|
+
* than in every host that wants to run one.
|
|
324
|
+
*
|
|
325
|
+
* `beginRequest` is required, and it comes from the application bundle for the
|
|
326
|
+
* reason in "Who owns the request" above. A listener built without one fails on
|
|
327
|
+
* its first request, which is the same trade `createFetchHandler` makes about
|
|
328
|
+
* `app.runMiddleware`: an optional lifecycle is a lifecycle somebody forgets,
|
|
329
|
+
* and what is lost when they do is every `after()` in the application.
|
|
330
|
+
*
|
|
331
|
+
* A handler that throws is answered with a bare 500 and reported on stderr:
|
|
332
|
+
* the body must not carry the stack, because the body goes to whoever asked,
|
|
333
|
+
* and stderr is where the operator is already looking. The drain is in a
|
|
334
|
+
* `finally` below the `catch`, so a middleware that logged the request sees its
|
|
335
|
+
* callback run once that 500 is on the wire rather than once the handler gave
|
|
336
|
+
* up — and a request that failed is still a request that happened, which is why
|
|
337
|
+
* it is drained at all.
|
|
338
|
+
*/
|
|
339
|
+
export function nodeListener(
|
|
340
|
+
handle: (request: Request) => Promise<Response>,
|
|
341
|
+
options: {|
|
|
342
|
+
readonly beginRequest: (request: Request) => RequestLifecycle,
|
|
343
|
+
readonly secure?: boolean,
|
|
344
|
+
|},
|
|
345
|
+
): (incoming: NodeRequest, outgoing: NodeResponse) => Promise<void> {
|
|
346
|
+
return async function listener(incoming: NodeRequest, outgoing: NodeResponse): Promise<void> {
|
|
347
|
+
// Declared out here because `toRequest` is inside the `try`: a request that
|
|
348
|
+
// could not even be built has no lifecycle to settle.
|
|
349
|
+
let lifecycle: RequestLifecycle | null = null;
|
|
350
|
+
try {
|
|
351
|
+
const request = toRequest(incoming, options);
|
|
352
|
+
lifecycle = options.beginRequest(request);
|
|
353
|
+
await lifecycle.run(async () => {
|
|
354
|
+
await send(outgoing, await handle(request));
|
|
355
|
+
});
|
|
356
|
+
} catch (error) {
|
|
357
|
+
console.error(error);
|
|
358
|
+
if (outgoing.headersSent) {
|
|
359
|
+
outgoing.destroy();
|
|
360
|
+
} else {
|
|
361
|
+
outgoing.statusCode = 500;
|
|
362
|
+
outgoing.setHeader("content-type", "text/plain; charset=utf-8");
|
|
363
|
+
outgoing.end("500 Internal Server Error\n");
|
|
364
|
+
}
|
|
365
|
+
} finally {
|
|
366
|
+
if (lifecycle != null) await lifecycle.settle();
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Static files, then the application: the whole of what a built uf app serves.
|
|
373
|
+
*
|
|
374
|
+
* `staticDir` is the directory `uf build` wrote — `dist/` in a checkout, and
|
|
375
|
+
* the `static/` copied beside `server.js` in an adapter's output.
|
|
376
|
+
*/
|
|
377
|
+
export function createServeHandler(options: {|
|
|
378
|
+
readonly staticDir: string,
|
|
379
|
+
readonly handle: (request: Request) => Promise<Response>,
|
|
380
|
+
|}): (request: Request) => Promise<Response> {
|
|
381
|
+
const serveStatic = createStaticHandler({ root: options.staticDir });
|
|
382
|
+
return async function handle(request: Request): Promise<Response> {
|
|
383
|
+
return (await serveStatic(request)) ?? (await options.handle(request));
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Serve the application until the process is stopped.
|
|
389
|
+
*
|
|
390
|
+
* What `uf build --adapter node` writes calls this and nothing else. It
|
|
391
|
+
* resolves once the socket is listening, with the address it took, because a
|
|
392
|
+
* caller that asked for port 0 has no other way to learn which port it got —
|
|
393
|
+
* and because a test that has to drive a deployed directory needs exactly
|
|
394
|
+
* that.
|
|
395
|
+
*
|
|
396
|
+
* `PORT` and `HOST` are read from the environment because that is how every
|
|
397
|
+
* process manager and container platform says which socket to take, and a
|
|
398
|
+
* production server that could only be told on the command line would need a
|
|
399
|
+
* wrapper script everywhere it ran. The command line wins over both, and the
|
|
400
|
+
* default address is every interface: a container that bound loopback would be
|
|
401
|
+
* a container nothing outside it can reach.
|
|
402
|
+
*/
|
|
403
|
+
export async function serve(options: {|
|
|
404
|
+
readonly staticDir: string,
|
|
405
|
+
readonly handle: (request: Request) => Promise<Response>,
|
|
406
|
+
/**
|
|
407
|
+
* The application bundle's own `beginRequest`.
|
|
408
|
+
*
|
|
409
|
+
* The generated `handler.js` re-exports it beside `fetch` so that
|
|
410
|
+
* `server.js` has one to pass; see "Who owns the request" above for why it
|
|
411
|
+
* cannot be imported here instead.
|
|
412
|
+
*/
|
|
413
|
+
readonly beginRequest: (request: Request) => RequestLifecycle,
|
|
414
|
+
readonly host?: string,
|
|
415
|
+
readonly port?: number,
|
|
416
|
+
|}): Promise<{|
|
|
417
|
+
readonly host: string,
|
|
418
|
+
readonly port: number,
|
|
419
|
+
readonly close: () => Promise<void>,
|
|
420
|
+
|}> {
|
|
421
|
+
const listener = nodeListener(
|
|
422
|
+
createServeHandler({ staticDir: options.staticDir, handle: options.handle }),
|
|
423
|
+
{ beginRequest: options.beginRequest },
|
|
424
|
+
);
|
|
425
|
+
const server = createServer((request, response) => {
|
|
426
|
+
void listener(request, response);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const host = options.host ?? argument("--host") ?? process.env.HOST ?? "0.0.0.0";
|
|
430
|
+
const port = options.port ?? Number(argument("--port") ?? process.env.PORT ?? 3000);
|
|
431
|
+
|
|
432
|
+
await new Promise((resolve, reject) => {
|
|
433
|
+
server.once("error", reject);
|
|
434
|
+
server.listen(port, host, resolve);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const address = server.address();
|
|
438
|
+
const bound = typeof address === "object" && address != null ? address.port : port;
|
|
439
|
+
// `0.0.0.0` is not a URL anybody can open, so the loopback spelling is what
|
|
440
|
+
// is printed — the same split `uf dev` and `uf start` print, and for the
|
|
441
|
+
// same reason: one of the two is a link and the other is a fact about the
|
|
442
|
+
// socket.
|
|
443
|
+
const shown = host === "0.0.0.0" || host === "::" ? "localhost" : host;
|
|
444
|
+
process.stdout.write(`uf: listening on http://${shown}:${String(bound)}\n`);
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
host,
|
|
448
|
+
port: bound,
|
|
449
|
+
close: () =>
|
|
450
|
+
new Promise((resolve) => {
|
|
451
|
+
server.close(() => resolve());
|
|
452
|
+
}),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** The value of a `--flag value` pair on the command line, if it is there. */
|
|
457
|
+
function argument(name: string): string | null {
|
|
458
|
+
const at = process.argv.indexOf(name);
|
|
459
|
+
return at === -1 ? null : (process.argv[at + 1] ?? null);
|
|
460
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uniflowed/server",
|
|
3
|
+
"version": "0.0.0-alpha.10",
|
|
4
|
+
"description": "Request-scoped server functions for the Unified Toolchain for Flow (React).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/ubugeeei-prod/uf.git",
|
|
11
|
+
"directory": "packages/server"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": "./index.js",
|
|
15
|
+
"./cache": "./cache.js",
|
|
16
|
+
"./edge": "./edge.js",
|
|
17
|
+
"./fetch": "./fetch.js",
|
|
18
|
+
"./host": "./host.js",
|
|
19
|
+
"./lambda": "./lambda.js",
|
|
20
|
+
"./node": "./node.js",
|
|
21
|
+
"./standalone": "./standalone.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"cache.js",
|
|
25
|
+
"edge.js",
|
|
26
|
+
"fetch.js",
|
|
27
|
+
"host.js",
|
|
28
|
+
"index.js",
|
|
29
|
+
"lambda.js",
|
|
30
|
+
"node.js",
|
|
31
|
+
"standalone.js",
|
|
32
|
+
"internal/*.js"
|
|
33
|
+
]
|
|
34
|
+
}
|