@uniflowed/vite 0.0.0-alpha.4 → 0.0.0-alpha.40

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.
@@ -0,0 +1,740 @@
1
+ // @noflow
2
+ //
3
+ // Plain JavaScript: executed by the host that serves a build.
4
+ //
5
+ // Serving what `uf build` wrote — one request handler, behind two front doors.
6
+ //
7
+ // `uf build` writes three things: a client bundle and prerendered HTML in
8
+ // `dist/`, and a server bundle in `.uf/build/server/server.js` that exports
9
+ // `render`, `dispatch`, `runMiddleware`, `routes`, `middleware`, `notFound`
10
+ // and `errors`. This module finds them, reads the client manifest, and hands
11
+ // both to the handler `uf preview` and `uf start` mount.
12
+ //
13
+ // `uf preview` and `uf start` are the two front doors, and they share
14
+ // everything below on purpose. A preview whose answers differ from the
15
+ // production server's is worse than no preview, because it is checked and
16
+ // believed. The difference between the two commands is which socket the
17
+ // handler is bolted to, not what it decides:
18
+ //
19
+ // preview — Vite's own preview server, with this handler behind its static
20
+ // middleware, so `vite.preview.proxy`, `vite.preview.https`,
21
+ // `headers` and `cors` are in effect and what is being checked is
22
+ // the build *as Vite serves it*.
23
+ // start — `node:http`, with no bundler in the process, because a host
24
+ // running a production build should not need Vite installed to
25
+ // answer a request.
26
+ //
27
+ // # Where the answering actually happens
28
+ //
29
+ // Not here, any more. Every decision about *what* a request is answered with
30
+ // lives in `@uniflowed/server` — `@uniflowed/server/fetch` for the application
31
+ // half and `@uniflowed/server/node` for the files and the socket — and this
32
+ // module is the part that is genuinely Vite's: finding the build on disk and
33
+ // reading the manifest a Vite build wrote.
34
+ //
35
+ // It moved because of `uf build --adapter`. `packages/server/serve.test.js` said
36
+ // what was wrong with the old arrangement while it was still the only one:
37
+ // "`internal/serve.js` is the seam a deploy adapter will need, and naming it
38
+ // in `exports` before one exists would be promising an interface nothing has
39
+ // used yet." An adapter exists now, and it may not import this package —
40
+ // `@uniflowed/vite` is the bundler, and the whole claim of deployable output
41
+ // is that the host needs neither the bundler nor the toolchain. So the seam is
42
+ // a package export of `@uniflowed/server`, and `uf preview`, `uf start` and
43
+ // every adapter now answer out of one implementation instead of copies that
44
+ // agree until they do not.
45
+ //
46
+ // # Why those imports are dynamic
47
+ //
48
+ // `@uniflowed/server` is Flow, and `driver.js` registers the loader hooks that
49
+ // make Flow importable *in its body* — after every static import in this graph
50
+ // has already been evaluated. So they are reached the same way the server
51
+ // bundle is: with `await import`, from [`loadBuild`], which is the point at
52
+ // which this process stops being plain JavaScript and starts being the
53
+ // project's.
54
+ //
55
+ // # Who owns the request
56
+ //
57
+ // The host does, and none of the three handlers below: each of them has a
58
+ // `Response` in hand rather than a response on the wire, and what `after()`
59
+ // promises is the wire. [`withRequest`] is the shape for a caller that writes
60
+ // into a Node response itself — `uf dev` and `uf preview` — and
61
+ // `@uniflowed/server/node`'s `nodeListener` does the same thing for `uf start`
62
+ // and for the `server.js` an adapter writes. Each of them begins the request
63
+ // with `entry.beginRequest`, runs the whole of answering it inside `run`, and
64
+ // settles it on the line after the last byte.
65
+ //
66
+ // It has to be the *entry's* `beginRequest` rather than one imported here: the
67
+ // request store is shared by every copy of one release of `@uniflowed/server`,
68
+ // and the release that matters is the one inside the application bundle. A
69
+ // host that resolved its own could be holding another release, would begin a
70
+ // request the application cannot see, and nothing would fail loudly — the
71
+ // guard would run, the page would render, and every `cookies()` in it would
72
+ // throw as though no host had run at all. See ubugeeei-prod/uf#389.
73
+
74
+ import { readFile, stat } from "node:fs/promises";
75
+ import path from "node:path";
76
+ import { pathToFileURL } from "node:url";
77
+
78
+ /**
79
+ * `@uniflowed/server`'s two halves, loaded once.
80
+ *
81
+ * Cached as the promise rather than the modules, so two concurrent callers
82
+ * share one import rather than racing to start two.
83
+ */
84
+ let deploymentModules = null;
85
+ function deployment() {
86
+ deploymentModules ??= Promise.all([
87
+ import("@uniflowed/server/fetch"),
88
+ import("@uniflowed/server/node"),
89
+ import("@uniflowed/server/cache"),
90
+ ]).then(([application, host, cache]) => ({ ...application, ...host, ...cache }));
91
+ return deploymentModules;
92
+ }
93
+
94
+ /**
95
+ * The cache `rendering.cache` describes, or `undefined` for no cache at all.
96
+ *
97
+ * `undefined` rather than a store with both switches off, and the difference is
98
+ * visible from an application: a handler with no `cache` installs nothing on
99
+ * the request, so `revalidateTag()` raises "there is not one here" instead of
100
+ * reporting that it expired nothing. A project that turned the cache off should
101
+ * be told that it did, not handed a cache that quietly does nothing.
102
+ *
103
+ * One store per server process, built when the handler is. With no
104
+ * `rendering.cache.store` that is the whole of what "in memory, per process"
105
+ * means in practice: `uf preview` and `uf start` each hold one, and two of them
106
+ * running at once share nothing. With one, the two processes share whatever the
107
+ * provider is in front of — see [`providerFor`].
108
+ *
109
+ * The store's remaining options are still not configurable from `uf.config.js`
110
+ * and are still not named here: `maxEntries` and `now` are facts about one
111
+ * process's heap and one process's clock, and a parameter threaded through for
112
+ * a setting nobody can set would be the shape of configurability with none of
113
+ * the substance.
114
+ *
115
+ * @param {{route?: boolean, fetch?: boolean, store?: string, storeDir?: string} | undefined} declared
116
+ * @param {(options?: object) => object} createCacheStore
117
+ * @param {{root: string, build: string | null}} where
118
+ */
119
+ async function cacheFor(declared, createCacheStore, where) {
120
+ const route = declared?.route === true;
121
+ const fetchCache = declared?.fetch === true;
122
+ if (!route && !fetchCache) return undefined;
123
+ const provider = await providerFor(declared, where);
124
+ const store =
125
+ provider == null ? createCacheStore() : createCacheStore({ provider, build: where.build });
126
+ return { store, route, fetch: fetchCache };
127
+ }
128
+
129
+ /**
130
+ * The durable provider `rendering.cache.store` names, or `null` for memory.
131
+ *
132
+ * Three answers, and the third is the one that matters to
133
+ * `docs/red-lines.md`'s third line. `"memory"` — the default, and what every
134
+ * project that says nothing gets — keeps the store exactly as it was.
135
+ * `"filesystem"` is uf's built-in, and it is a convenience rather than an
136
+ * architecture. Anything else is a **module specifier**, resolved from the
137
+ * project, exporting `createCacheProvider`: the same shape `builder.module`
138
+ * has, chosen for the same reason that document gives — "a provider a project
139
+ * can replace has to be a name it can write, and an enum with one variant
140
+ * cannot become one without a release of uf".
141
+ *
142
+ * So a project with a Redis, a KV namespace or an S3 bucket writes twenty lines
143
+ * against `@uniflowed/server/cache`'s `CacheProvider` type, names the module
144
+ * here, and uf never learns which of those it was.
145
+ *
146
+ * # Why a missing build identity is a refusal
147
+ *
148
+ * Because the alternatives are both worse. Falling back to memory would give a
149
+ * project that asked for a cache surviving restarts one that does not, and the
150
+ * symptom is a `MISS` on every cold request — indistinguishable from a cache
151
+ * that is simply cold. Generating an identity per process would be worse again:
152
+ * four servers would write four copies of everything into one directory and
153
+ * read none of each other's. The deployment rules say a target that cannot
154
+ * provide a durable store has to say so, and this is a host saying so.
155
+ *
156
+ * @param {{store?: string, storeDir?: string} | undefined} declared
157
+ * @param {{root: string, build: string | null}} where
158
+ */
159
+ async function providerFor(declared, { root, build, regenerates }) {
160
+ // A build that regenerates pages keeps what it regenerated on disk unless the
161
+ // project named a store. A restart that took every regenerated page back to
162
+ // the build's copy would be a server whose pages went back in time, which is
163
+ // the one thing regeneration is for not doing. `"memory"`, said out loud, is
164
+ // still memory.
165
+ const named = declared?.store ?? (regenerates === true ? "filesystem" : "memory");
166
+ if (named === "memory") return null;
167
+ if (build == null) {
168
+ throw new Error(
169
+ `uf: rendering.cache.store is ${JSON.stringify(named)}, which keeps entries between ` +
170
+ "restarts, and there is no build identity to key them by. `uf build` writes one " +
171
+ "beside the server bundle; set UF_BUILD_ID to name it yourself. Without one, a " +
172
+ "deploy would answer the new build's URLs with the previous build's documents.",
173
+ );
174
+ }
175
+ const directory = path.resolve(root, declared?.storeDir ?? path.join(".uf", "cache", "route"));
176
+ if (named === "filesystem") {
177
+ const { createFilesystemCache } = await import("@uniflowed/server/cache/filesystem");
178
+ return createFilesystemCache({ directory });
179
+ }
180
+ const provider = await import(providerSpecifier(root, named));
181
+ const create = provider.createCacheProvider ?? provider.default;
182
+ if (typeof create !== "function") {
183
+ throw new Error(
184
+ `uf: rendering.cache.store names ${JSON.stringify(named)}, which exports no ` +
185
+ "`createCacheProvider`. A durable cache provider is a module exporting that " +
186
+ "function; see @uniflowed/server/cache's CacheProvider type for what it returns.",
187
+ );
188
+ }
189
+ return create({ build, directory });
190
+ }
191
+
192
+ /**
193
+ * Everything a served build consists of.
194
+ *
195
+ * Read once at startup rather than per request: the manifest does not change
196
+ * while the server runs, and importing the server bundle again per request
197
+ * would re-evaluate every module in the application.
198
+ *
199
+ * `@uniflowed/server` is loaded here too, and not lazily on the first request:
200
+ * a missing or broken install should fail the command that starts the server,
201
+ * with the message the import raises, rather than a minute later inside
202
+ * whichever request happened to arrive first.
203
+ *
204
+ * @param {{root: string, outDir: string, serverDir: string}} build
205
+ */
206
+ export async function loadBuild({ root, outDir, serverDir }) {
207
+ const distDir = path.resolve(root, outDir);
208
+ const entryFile = path.join(path.resolve(root, serverDir), "server.js");
209
+
210
+ // Named separately, because the two failures have different fixes and a
211
+ // combined "run uf build" would be wrong for one of them: a `dist/` with no
212
+ // server bundle beside it is what a `.uf/` that was cleaned looks like.
213
+ await readable(entryFile, `the server bundle is missing at ${entryFile}`);
214
+ const manifestFile = path.join(distDir, ".vite", "manifest.json");
215
+ await readable(manifestFile, `the client manifest is missing at ${manifestFile}`);
216
+
217
+ const manifest = JSON.parse(await readFile(manifestFile, "utf8"));
218
+ const entry = await import(pathToFileURL(entryFile).href);
219
+ await deployment();
220
+ const build = await buildIdentity(root, serverDir);
221
+ const regeneration = await readRegeneration(path.resolve(root, serverDir));
222
+ return {
223
+ entry,
224
+ assets: await documentAssetsFor(path.resolve(root, serverDir), manifest),
225
+ distDir,
226
+ root,
227
+ build,
228
+ regeneration,
229
+ };
230
+ }
231
+
232
+ /** What `uf build` records a document's tags in, beside the server bundle. */
233
+ export const DOCUMENT_ASSETS_FILE = "uf-document-assets.json";
234
+
235
+ /**
236
+ * The tags a served document needs: the ones `uf build` recorded, or — for a
237
+ * build from before it recorded them — the client manifest's.
238
+ *
239
+ * Recorded rather than recomputed, because the client manifest is no longer the
240
+ * whole answer. An application React Server Components render links the
241
+ * stylesheets its rsc graph emitted and the ones each client module's chunk
242
+ * carries, and neither is reachable from the client entry the manifest is walked
243
+ * from — so a server that recomputed the tags rendered every page without the
244
+ * stylesheets its prerendered pages had. `uf start`, `uf preview`, `--adapter`
245
+ * and `--compile` all read them from here.
246
+ *
247
+ * @param {string} serverDir absolute path of the server bundle's directory
248
+ * @param {object} manifest the client build's Vite manifest
249
+ */
250
+ export async function documentAssetsFor(serverDir, manifest) {
251
+ const file = path.join(serverDir, DOCUMENT_ASSETS_FILE);
252
+ let recorded;
253
+ try {
254
+ recorded = await readFile(file, "utf8");
255
+ } catch {
256
+ return assetsFromManifest(manifest);
257
+ }
258
+ try {
259
+ return JSON.parse(recorded);
260
+ } catch {
261
+ throw new Error(`uf: ${file} is not the JSON \`uf build\` writes; run \`uf build\` again`);
262
+ }
263
+ }
264
+
265
+ /**
266
+ * The file beside the server bundle that names the pages a build regenerates.
267
+ *
268
+ * Written by `driver.js`'s build, and only for a build that has such a page.
269
+ */
270
+ export const REGENERATION_FILE = "regenerate.json";
271
+
272
+ /**
273
+ * Where a regenerated page's document goes, under the build's output directory.
274
+ *
275
+ * Somewhere no static half answers the page's own URL, which is the point: a
276
+ * file at `dist/posts/a/index.html` would be served by every host before the
277
+ * server saw the request, forever, whatever the page's lifetime said.
278
+ */
279
+ export const REGENERATED_DIRECTORY = "__uf/regenerate";
280
+
281
+ /**
282
+ * The pages this build regenerates, or `undefined` for a build that has none.
283
+ *
284
+ * `undefined` rather than an empty manifest, so a build with nothing to
285
+ * regenerate serves exactly as every build did before regeneration existed.
286
+ */
287
+ export async function readRegeneration(serverDir) {
288
+ let text;
289
+ try {
290
+ text = await readFile(path.join(serverDir, REGENERATION_FILE), "utf8");
291
+ } catch (error) {
292
+ if (error?.code === "ENOENT") return undefined;
293
+ throw error;
294
+ }
295
+ return JSON.parse(text);
296
+ }
297
+
298
+ /**
299
+ * What `import()` should be given for a provider a project named.
300
+ *
301
+ * A relative path in `uf.config.js` is relative to *the project*, which is what
302
+ * anybody writing `"./cache/redis.js"` means and is not what `import()` from
303
+ * this module would do — it would look beside `@uniflowed/vite`, find nothing,
304
+ * and report a missing module the config file does not mention. `builder.module`
305
+ * settled the same question the same way in `uf_cli`'s `project_directory`.
306
+ *
307
+ * A bare specifier is left alone: `"@acme/uf-cache-redis"` is a package, and
308
+ * resolving it is Node's job and not this function's.
309
+ *
310
+ * @param {string} root
311
+ * @param {string} named
312
+ */
313
+ export function providerSpecifier(root, named) {
314
+ if (!named.startsWith(".") && !path.isAbsolute(named)) return named;
315
+ return pathToFileURL(path.resolve(root, named)).href;
316
+ }
317
+
318
+ /** What `uf build` writes its identity into, beside the server bundle. */
319
+ export const BUILD_ID_FILE = "uf-build-id";
320
+
321
+ /**
322
+ * The identity of the build being served, or `null`.
323
+ *
324
+ * Only a durable cache reads it, and only a durable cache needs it: an entry
325
+ * that cannot outlive the process cannot outlive the build either, so every
326
+ * command that keeps its cache in memory is entitled to `null` here and never
327
+ * looks. `packages/server/internal/cache-key.js` argues the rest.
328
+ *
329
+ * `UF_BUILD_ID` first, then the file `uf build` wrote. The environment wins for
330
+ * the reason it wins in `crates/uf_rsc`'s `BuildId::from_env_or_generate`,
331
+ * which reads the same variable for the same kind of fact: a deployment that
332
+ * needs two artefacts to *be* one build — a blue/green pair, a rebuild of a
333
+ * tagged commit — has no other way to say so.
334
+ *
335
+ * `null` rather than a generated fallback, and that is the whole point of the
336
+ * function. A per-process identity would give four servers four caches with a
337
+ * shared disk between them, which is worse than four memories: it would write
338
+ * four copies of everything and read none of them. Whoever asked for a durable
339
+ * store is told there is no build to key it by, and gets to fix it.
340
+ */
341
+ export async function buildIdentity(root, serverDir) {
342
+ const named = process.env.UF_BUILD_ID;
343
+ if (typeof named === "string" && named !== "") return named;
344
+ try {
345
+ const file = path.join(path.resolve(root, serverDir), BUILD_ID_FILE);
346
+ const value = (await readFile(file, "utf8")).trim();
347
+ return value === "" ? null : value;
348
+ } catch {
349
+ return null;
350
+ }
351
+ }
352
+
353
+ async function readable(file, message) {
354
+ try {
355
+ await stat(file);
356
+ } catch {
357
+ throw new Error(`uf: ${message}; run \`uf build\` first`);
358
+ }
359
+ }
360
+
361
+ /**
362
+ * The tags a rendered document needs, from the client build's manifest.
363
+ *
364
+ * Two walks over the manifest, because the two answers are different. A
365
+ * `modulepreload` is worth emitting only for a chunk this document will
366
+ * certainly load, which is the entry's *static* imports. A stylesheet has to
367
+ * be emitted for anything the page might render, and the router loads every
368
+ * route module dynamically — so a stylesheet imported by a layout is reached
369
+ * through `dynamicImports` and through nothing else. Following only the static
370
+ * graph, as this did, meant a layout could import a stylesheet and the built
371
+ * HTML would silently ship without it.
372
+ *
373
+ * The cost is that a project with per-route stylesheets links all of them on
374
+ * every page. Narrowing that needs the route table to say which chunk each
375
+ * route came from, which the manifest alone cannot tell us.
376
+ *
377
+ * The entry is found by its `isEntry` flag rather than by key, because a
378
+ * virtual module's manifest key is an implementation detail of the bundler.
379
+ *
380
+ * This is the one piece of serving a build that is genuinely Vite's — a Vite
381
+ * manifest, read the way Vite writes it — which is why it stayed behind when
382
+ * the rest moved to `@uniflowed/server`. `uf build --adapter` calls it too,
383
+ * at build time, and bakes the answer into what it emits.
384
+ */
385
+ export function assetsFromManifest(manifest, base = "") {
386
+ // `client` by name first. An application React Server Components render
387
+ // gives the client build one entry per client module as well, and the
388
+ // document's script is the application's entry, not whichever of those the
389
+ // manifest happens to list first.
390
+ const chunks = Object.values(manifest);
391
+ const entry =
392
+ chunks.find((chunk) => chunk.isEntry && chunk.name === "client") ??
393
+ chunks.find((chunk) => chunk.isEntry);
394
+ if (entry == null) throw new Error("uf: the client manifest has no entry chunk");
395
+
396
+ const styles = new Set(entry.css ?? []);
397
+ const seen = new Set();
398
+ const collectStyles = (chunk) => {
399
+ for (const imported of [...(chunk.imports ?? []), ...(chunk.dynamicImports ?? [])]) {
400
+ if (seen.has(imported)) continue;
401
+ seen.add(imported);
402
+ const dependency = manifest[imported];
403
+ if (dependency == null) continue;
404
+ for (const css of dependency.css ?? []) styles.add(css);
405
+ collectStyles(dependency);
406
+ }
407
+ };
408
+ collectStyles(entry);
409
+
410
+ const preloads = new Set();
411
+ const collectPreloads = (chunk) => {
412
+ for (const imported of chunk.imports ?? []) {
413
+ const dependency = manifest[imported];
414
+ if (dependency == null || preloads.has(dependency.file)) continue;
415
+ preloads.add(dependency.file);
416
+ collectPreloads(dependency);
417
+ }
418
+ };
419
+ collectPreloads(entry);
420
+
421
+ // Under `app.router.basePath` when there is one: a prerendered document is
422
+ // written outside Vite's HTML transform, so nothing else would put it there.
423
+ return {
424
+ scripts: [`${base}/${entry.file}`],
425
+ styles: [...styles].map((file) => `${base}/${file}`),
426
+ preloads: [...preloads].map((file) => `${base}/${file}`),
427
+ };
428
+ }
429
+
430
+ /**
431
+ * Answer one request inside it, and settle it when the answer has been written.
432
+ *
433
+ * `body` is everything that decides the response *and writes it*; this is the
434
+ * line after. `settle` is in a `finally` because a request that failed is
435
+ * still a request that happened: a middleware that logged the arrival is owed
436
+ * its callback whether the render threw or not, and `drainDeferred` already
437
+ * reports a failing task rather than propagating it.
438
+ *
439
+ * `entry.beginRequest` and not an import: the request store is shared by every
440
+ * copy of one release of `@uniflowed/server`, and the release that matters is
441
+ * the one inside the application bundle. See `serverModuleSource` in
442
+ * `./routes.js`.
443
+ *
444
+ * The one case this cannot be exact about is a request uf hands back rather
445
+ * than answers: a caller whose `catch` is `next(error)` gives the response to
446
+ * Vite's chain, which writes a 500 at a moment nothing here can observe, so
447
+ * such a request settles when uf lets go of it. `nodeListener` and the
448
+ * compiled binary write their own failures and settle after them. It is worth
449
+ * naming rather than papering over, and it is the failure path of a request
450
+ * that already went wrong — not the ordinary one this exists for.
451
+ *
452
+ * @param {{beginRequest: (request: Request) => {context: object, run: <T>(body: () => Promise<T>) => Promise<T>, settle: () => Promise<void>}}} entry
453
+ * @param {Request} request
454
+ * @param {() => Promise<mixed>} body
455
+ */
456
+ export async function withRequest(entry, request, body) {
457
+ const { run, settle } = await beginRequest(entry, request);
458
+ try {
459
+ return await run(body);
460
+ } finally {
461
+ await settle();
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Begin a request on this host, with what this host can do already on it.
467
+ *
468
+ * The half of [`withRequest`] that a caller which may *not* answer needs.
469
+ * `uf dev` runs the application's middleware, its action endpoint and its
470
+ * dispatcher for every request, and hands the ones none of them claimed back
471
+ * to Vite's chain — at which point the response is written somewhere this
472
+ * module cannot see, so settling has to wait for the socket rather than for a
473
+ * `finally` here. A caller that always answers should use [`withRequest`] and
474
+ * not think about it.
475
+ *
476
+ * `entry.beginRequest` and not an import: the request store is shared by every
477
+ * copy of one release of `@uniflowed/server`, and the release that matters is
478
+ * the one inside the application bundle. See `serverModuleSource` in
479
+ * `./routes.js`.
480
+ *
481
+ * What this host can do is put on the request the way `createFetchHandler`
482
+ * puts it on the one it owns. `uf dev` and `uf build --compile` reach a route
483
+ * handler without going through that function, and a handler that streams
484
+ * events or queues work has to get the same answer from all four front doors —
485
+ * a capability that is present under `uf start` and absent under `uf dev` is
486
+ * the difference this whole seam exists to remove.
487
+ *
488
+ * `nodeCapabilities`, because both of those *are* a Node process with a
489
+ * socket: a body reaches the client as it is written, and the process is still
490
+ * there afterwards. Neither passes an upgrader or a queue, because uf defines
491
+ * both and implements neither.
492
+ *
493
+ * @param {{beginRequest: (request: Request) => {context: object, run: <T>(body: () => Promise<T>) => Promise<T>, settle: () => Promise<void>}}} entry
494
+ * @param {Request} request
495
+ */
496
+ export async function beginRequest(entry, request) {
497
+ const lifecycle = entry.beginRequest(request);
498
+ const { nodeCapabilities } = await deployment();
499
+ lifecycle.context.capabilities ??= nodeCapabilities();
500
+ return lifecycle;
501
+ }
502
+
503
+ /**
504
+ * The application half: route handlers, then rendering.
505
+ *
506
+ * `@uniflowed/server/fetch`'s `createFetchHandler`, reached through the
507
+ * dynamic import above. Kept as a function here — rather than making every
508
+ * caller await the module — because the two servers construct their handler
509
+ * before they take a socket, and an `await` in that position would put the
510
+ * import between the port and the first request rather than before both.
511
+ *
512
+ * It must be called inside a request its caller began; it begins none, because
513
+ * it has a `Response` in hand and not a response on the wire. A caller that
514
+ * forgets is not left to discover it: `entry.runMiddleware` refuses outside a
515
+ * request and names what establishes one. See "Who owns the request" above.
516
+ *
517
+ * `cache` is `rendering.cache` from `uf.config.js`, straight through: this is
518
+ * the point where four switches that used to reach a JSON file and nothing else
519
+ * become a store a request can hit. See ubugeeei-prod/uf#277.
520
+ *
521
+ * `root` and `build` come with it, and only the cache reads either: `root` is
522
+ * where a `storeDir` is resolved from and `build` is what a durable entry is
523
+ * keyed by. Both are `undefined` for a caller that constructs a handler by
524
+ * hand, which is the memory-only store and needs neither.
525
+ *
526
+ * @param {{entry: object, assets: object, cache?: object, root?: string, build?: string | null}} build
527
+ */
528
+ export function createApplicationHandler({ entry, assets, cache, root, build, regeneration }) {
529
+ const ready = deployment().then(
530
+ async ({ createFetchHandler, createCacheStore, nodeCapabilities }) =>
531
+ createFetchHandler({
532
+ app: entry,
533
+ document: assets,
534
+ cache: await cacheFor(cache, createCacheStore, {
535
+ root: root ?? process.cwd(),
536
+ build: build ?? null,
537
+ regenerates: regeneration != null,
538
+ }),
539
+ // The pages this build regenerates, from the manifest beside the server
540
+ // bundle. Absent for a build with none, which then serves exactly as it
541
+ // did before regeneration existed.
542
+ ...(regeneration == null ? {} : { regeneration }),
543
+ // `uf preview` and `uf start` are a Node process with a socket, which is
544
+ // what a deployed `--adapter node` build is too — so a route handler
545
+ // that streams events answers the same way in the preview it is checked
546
+ // in and in the deployment it ends up as. See `withRequest` above.
547
+ capabilities: nodeCapabilities(),
548
+ }),
549
+ );
550
+ return async function handle(request) {
551
+ return (await ready)(request);
552
+ };
553
+ }
554
+
555
+ /**
556
+ * The static half: a file under `root`, or `null` for the caller to carry on.
557
+ *
558
+ * `@uniflowed/server/node`'s, for the same reason as above: what a deployment
559
+ * runs and what `uf start` runs have to be the same code, not the same idea.
560
+ */
561
+ export function createStaticHandler({ root }) {
562
+ const ready = deployment().then(({ createStaticHandler: create }) => create({ root }));
563
+ return async function serveStatic(request) {
564
+ return (await ready)(request);
565
+ };
566
+ }
567
+
568
+ /**
569
+ * Static files, then the application: the whole of what a built uf app serves.
570
+ *
571
+ * Static first, and that ordering is a compatibility requirement rather than a
572
+ * preference. Vite's preview server runs its own file middleware before
573
+ * anything added afterwards can see the request, so `uf preview` serves a file
574
+ * first whether or not this agrees — and `uf start` disagreeing would mean a
575
+ * project whose handler path collides with a file in `public/` behaves one way
576
+ * when it is checked and the other way when it is deployed.
577
+ *
578
+ * It is `@uniflowed/server/node`'s own `createServeHandler`, the one a deployed
579
+ * `server.js` runs, rather than the two halves composed a second time here.
580
+ * That one also hands the application the build's files when the static half
581
+ * has nothing, which is how a regenerated page starts from the document the
582
+ * build wrote; a composition of its own here would be a `uf start` whose
583
+ * regenerated pages rendered on their first request while a deployment's did
584
+ * not.
585
+ *
586
+ * It is handed the bundle's `routing` too, so `app.router`'s redirects answer
587
+ * before the files and its headers go on whatever answers, here exactly as in
588
+ * a deployment. `uf preview` puts the same two in front of Vite's file
589
+ * middleware as well; see [`answerRouting`].
590
+ *
591
+ * @param {{entry: object, assets: object, distDir: string, cache?: object, root?: string, build?: string | null, regeneration?: object}} build
592
+ */
593
+ export function createServeHandler({ entry, assets, distDir, cache, root, build, regeneration }) {
594
+ const application = createApplicationHandler({ entry, assets, cache, root, build, regeneration });
595
+ const ready = deployment().then(({ createServeHandler: create }) =>
596
+ create({ staticDir: distDir, handle: application, routing: entry.routing }),
597
+ );
598
+ return async function handle(request) {
599
+ return (await ready)(request);
600
+ };
601
+ }
602
+
603
+ /**
604
+ * Whether `routing` has anything to say in front of a file server.
605
+ *
606
+ * Redirects and headers are the two that do; a rewrite is the application's.
607
+ * Asked before a middleware is mounted at all, so a project with no rules pays
608
+ * nothing per request under `uf dev` or `uf preview`.
609
+ *
610
+ * @param {{redirects?: unknown[], headers?: unknown[]} | undefined} routing
611
+ */
612
+ export function answersInFrontOfFiles(routing) {
613
+ return (
614
+ (routing?.redirects?.length ?? 0) > 0 ||
615
+ (routing?.headers?.length ?? 0) > 0 ||
616
+ (routing?.basePath ?? "") !== "" ||
617
+ (routing?.trailingSlash ?? "ignore") !== "ignore"
618
+ );
619
+ }
620
+
621
+ /**
622
+ * `app.router`'s headers and redirects, for a door whose files Vite serves.
623
+ *
624
+ * `uf dev` and `uf preview` mount this in front of Vite's own middleware:
625
+ * the headers are pinned on the Node response, so they survive the
626
+ * `writeHead` Vite's file server writes its own with, and a redirect is
627
+ * answered before any file is looked for. `true` when it answered.
628
+ *
629
+ * @param {object} routing the bundle's `routing`
630
+ * @param {Request} request the address alone; see `toAddressRequest`
631
+ * @param {import("node:http").ServerResponse} response
632
+ */
633
+ export async function answerRouting(routing, request, response) {
634
+ const { admit, headersFor, pinHeaders, send: write } = await deployment();
635
+ pinHeaders(response, headersFor(routing, request));
636
+ // A request outside the base path, the other spelling of a path, or a
637
+ // redirect rule: answered here, before Vite's own base middleware would
638
+ // answer the first in its words rather than uf's.
639
+ const admitted = admit(routing, request);
640
+ if (admitted.kind !== "answer") return false;
641
+ await write(response, admitted.response);
642
+ return true;
643
+ }
644
+
645
+ /**
646
+ * A request [`answerRouting`] let through, spelled so Vite's own middleware
647
+ * recognises it.
648
+ *
649
+ * Vite serves under its `base` with the trailing slash, `/docs/`, and its base
650
+ * middleware answers every other path with a 404 of its own, the bare `/docs`
651
+ * included. But `/docs` is the application's root under `app.router.basePath`,
652
+ * and the only spelling of it unless the trailing-slash policy is `"always"`,
653
+ * which has already answered `/docs` with a `308` by the time this is asked.
654
+ * So a request for exactly the base goes on to Vite as `/docs/`, which Vite
655
+ * takes the base off and hands on as the root. The application is handed the
656
+ * root either way.
657
+ *
658
+ * @param {{basePath?: string} | undefined} routing the bundle's `routing`
659
+ * @param {import("node:http").IncomingMessage} request
660
+ */
661
+ export function forViteBase(routing, request) {
662
+ const base = routing?.basePath ?? "";
663
+ const url = request.url ?? "/";
664
+ if (base === "") return;
665
+ const queryAt = url.indexOf("?");
666
+ const pathname = queryAt === -1 ? url : url.slice(0, queryAt);
667
+ if (pathname === base) {
668
+ request.url = `${base}/${queryAt === -1 ? "" : url.slice(queryAt)}`;
669
+ }
670
+ }
671
+
672
+ /**
673
+ * `app.router.rewrites` for this request, for `uf dev`: the rewritten request,
674
+ * or `null`.
675
+ *
676
+ * `@uniflowed/server`'s `rewriteFor`, which `createFetchHandler` asks for every
677
+ * other front door at the same point — after the files, before the guard.
678
+ *
679
+ * @param {object | undefined} routing the bundle's `routing`
680
+ * @param {Request} request
681
+ */
682
+ export async function rewriteRouting(routing, request) {
683
+ const { rewriteFor } = await deployment();
684
+ return rewriteFor(routing, request);
685
+ }
686
+
687
+ /**
688
+ * Whether a file server may answer this request, or uf has to go first.
689
+ *
690
+ * `@uniflowed/server`'s `prerenderedMayAnswer`, reached the same way
691
+ * `createStaticHandler` is. It exists out here because of the one request
692
+ * `uf preview` may not leave to Vite: `createServeHandler` above is mounted
693
+ * *behind* Vite's static middleware, which is fine for everything except a
694
+ * request carrying the draft cookie. `createStaticHandler` declines a
695
+ * prerendered document for such a request and, under `uf preview`, never sees
696
+ * it — so draft mode appeared to be off there while it worked under `uf dev`
697
+ * and `uf start`. That is ubugeeei-prod/uf#620.
698
+ *
699
+ * `driver.js` asks this in a middleware mounted in *front* of Vite's, and
700
+ * mounts `createServeHandler` behind it for the answer, so a draft request
701
+ * goes through the same handler `uf start` uses and gets the same answer —
702
+ * including its stylesheets and chunks, which that handler still serves off
703
+ * disk. Every other request is untouched and Vite's file middleware runs as
704
+ * before.
705
+ *
706
+ * A gate rather than a second handler, because the caller has a request
707
+ * lifecycle to open and must not open one for a request it is about to hand
708
+ * on.
709
+ */
710
+ export function createPrerenderGate() {
711
+ const ready = deployment().then(({ prerenderedMayAnswer }) => prerenderedMayAnswer);
712
+ return async function prerenderedMayAnswer(cookieHeader) {
713
+ return (await ready)(cookieHeader ?? null);
714
+ };
715
+ }
716
+
717
+ /**
718
+ * A `Request`/`Response` handler as a Node request listener.
719
+ *
720
+ * `@uniflowed/server/node`'s, which is also what the `server.js` an adapter
721
+ * writes runs — so a request reaching `uf start` and the same request reaching
722
+ * a deployed directory go through one translation rather than two, and settle
723
+ * at one moment rather than at two.
724
+ *
725
+ * `entry` is the second argument rather than something this reaches for: it is
726
+ * the application bundle's own `beginRequest` that has to own the request, for
727
+ * the reason in "Who owns the request" above. It is required, and a listener
728
+ * built without one fails on its first request — the same trade
729
+ * `createFetchHandler` makes about `app.runMiddleware`, and for the same
730
+ * reason: an optional lifecycle is a lifecycle somebody forgets, and what is
731
+ * lost when they do is every `after()` in the application.
732
+ */
733
+ export function nodeListener(handle, entry) {
734
+ const ready = deployment().then(({ nodeListener: create }) =>
735
+ create(handle, { beginRequest: entry.beginRequest }),
736
+ );
737
+ return async function listener(incoming, outgoing) {
738
+ return (await ready)(incoming, outgoing);
739
+ };
740
+ }