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

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