@uniflowed/vite 0.0.0-alpha.13 → 0.0.0-alpha.14

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,366 @@
1
+ // @noflow
2
+ //
3
+ // Plain JavaScript: executed by the host that runs Vite, before any transform.
4
+ //
5
+ // The channel a browser reports on.
6
+ //
7
+ // Every other uf diagnostic — a type error, a lint finding, a failing test, a
8
+ // page that rendered its error boundary — arrives in the terminal the
9
+ // developer already has open. A diagnostic the *browser* produces had nowhere
10
+ // to go: the page is the only process that knows about it, and it has no
11
+ // channel back. So it lived in a browser overlay, which has to be noticed, in
12
+ // a window that may not be in front, by somebody who does not know to look.
13
+ //
14
+ // This is that channel, and it is deliberately one channel rather than one per
15
+ // feature. Two things report on it today:
16
+ //
17
+ // * `POST /__uf/diagnostic` — a diagnostic a browser-side runtime produced
18
+ // and wants a person to read. `@uniflowed/router`'s `internal/diagnostics.js`
19
+ // is the client half, and the hydration-mismatch report beside it is what
20
+ // calls it.
21
+ // * `POST /__uf/vitals` — the five numbers `@uniflowed/web/vitals`
22
+ // measures, posted by `vitalsBeacon()`. In production a project points the
23
+ // beacon at an endpoint of its own; in development there was nothing at
24
+ // the default path, so the one place the numbers are most useful — while
25
+ // you are looking at the page — was the one place they went nowhere.
26
+ //
27
+ // Both end up as the same `diagnostic` event on the driver's control channel
28
+ // (see `./events.js`), which is what gets them uf's own rendering: a severity,
29
+ // a location, and a code frame when the browser had a position to give.
30
+ //
31
+ // # The terminal, and not a browser overlay
32
+ //
33
+ // ubugeeei-prod/uf#557 asked for the vitals to be shown in the overlay. They
34
+ // are shown in the terminal instead, and the later issue that generalised this
35
+ // — #583 — is the argument: a diagnostic that exists only in a browser window
36
+ // has to be noticed by somebody who does not know to look, which is the defect
37
+ // rather than the delivery. Sending these *back* to an overlay would also be
38
+ // circular for the diagnostic half, which arrived from the page in the first
39
+ // place, and an overlay covers the page a performance number is about. What it
40
+ // costs is that a reader watching the browser rather than the terminal sees
41
+ // nothing until they look, which is where every other uf diagnostic already
42
+ // is.
43
+ //
44
+ // # Nothing leaves the machine
45
+ //
46
+ // This module opens no connection. It reads a request that the page on the
47
+ // other end of the dev server's own socket made, writes a line to the terminal
48
+ // that started the dev server, and answers `204`. There is no destination, no
49
+ // third party and nothing to configure, in development or otherwise.
50
+ //
51
+ // # Why the paths are written out here
52
+ //
53
+ // `VITALS_ENDPOINT` in `@uniflowed/web/vitals` and `DIAGNOSTIC_ENDPOINT` in
54
+ // `@uniflowed/router`'s `internal/diagnostics.js` are the same two strings, and
55
+ // they are the contract. They cannot be *imported* here: this module is loaded
56
+ // by Vite before any Flow transform exists, and both of those are Flow. So they
57
+ // are written out, and `tests/library/dev-channel.test.js` asserts that all
58
+ // four spellings agree — a duplicated constant with a test on it is honest, and
59
+ // one without is how the browser ends up posting to a path nothing serves.
60
+ //
61
+ // # Why `/__uf/`
62
+ //
63
+ // A directory under `app/` whose name begins with `_` is not a route, so no
64
+ // application can put anything at this prefix and nothing here can shadow a
65
+ // path a project wrote. That is what makes it safe as a default destination
66
+ // and available to the dev server.
67
+
68
+ /** Where `@uniflowed/router`'s `reportDiagnostic` posts. */
69
+ export const DIAGNOSTIC_ENDPOINT = "/__uf/diagnostic";
70
+
71
+ /** Where `@uniflowed/web/vitals`'s `vitalsBeacon()` posts by default. */
72
+ export const VITALS_ENDPOINT = "/__uf/vitals";
73
+
74
+ /**
75
+ * The most a report may weigh.
76
+ *
77
+ * A diagnostic is a headline and a few lines of context; a vitals report is
78
+ * five numbers. Neither is close to this, and the ceiling is here because the
79
+ * body arrives from a page — "no unbounded anything" in `docs/security.md`
80
+ * covers a dev server reading a request as much as it covers a production one,
81
+ * and a page with a runaway loop in it must not be able to make `uf dev` grow
82
+ * without bound.
83
+ */
84
+ export const MAX_BODY_BYTES = 64 * 1024;
85
+
86
+ /** The most detail lines one diagnostic prints. */
87
+ const MAX_DETAIL_LINES = 40;
88
+
89
+ /** The most characters any single line of a diagnostic prints. */
90
+ const MAX_LINE_CHARS = 400;
91
+
92
+ /** The most metrics one vitals report is read for; there are five names. */
93
+ const MAX_VITALS = 16;
94
+
95
+ /** The most characters a metric's name or its rating may print as. */
96
+ const MAX_NAME_CHARS = 32;
97
+
98
+ /** The severities the channel accepts, and the words the terminal uses. */
99
+ const SEVERITIES = new Set(["error", "warn", "info"]);
100
+
101
+ /**
102
+ * The connect middleware that answers the channel.
103
+ *
104
+ * Mounted **before** the application middleware, so a request under `/__uf/`
105
+ * never reaches a project's `_uf.middleware.js` or its route table. A guard
106
+ * that ran for a page's own telemetry would be a guard asked a question the
107
+ * application never asks, and one that redirected it would turn a report into
108
+ * a login page.
109
+ *
110
+ * `report` is injected rather than reached for so that this module can be
111
+ * driven without a terminal, a socket or a driver; `internal/events.js`'s
112
+ * `emit` is what the plugin passes.
113
+ *
114
+ * @param {(diagnostic: object) => void} report
115
+ */
116
+ export function createChannelMiddleware(report) {
117
+ return async function channel(request, response, next) {
118
+ const pathname = (request.url ?? "/").split("?")[0];
119
+ const isVitals = pathname === VITALS_ENDPOINT;
120
+ if (!isVitals && pathname !== DIAGNOSTIC_ENDPOINT) {
121
+ next();
122
+ return;
123
+ }
124
+
125
+ // A `GET` on either path is somebody checking whether the dev server has
126
+ // them, and `405` with `Allow` answers that exactly. `404` would have said
127
+ // the opposite of the truth.
128
+ if (request.method !== "POST") {
129
+ response.statusCode = 405;
130
+ response.setHeader("allow", "POST");
131
+ response.end();
132
+ return;
133
+ }
134
+
135
+ let body;
136
+ try {
137
+ body = await readBody(request, MAX_BODY_BYTES);
138
+ } catch {
139
+ // A socket that went away mid-body. There is nothing to report and
140
+ // nobody left to answer.
141
+ response.statusCode = 400;
142
+ response.end();
143
+ return;
144
+ }
145
+ if (body == null) {
146
+ response.statusCode = 413;
147
+ response.end();
148
+ return;
149
+ }
150
+
151
+ let payload = null;
152
+ try {
153
+ payload = JSON.parse(body);
154
+ } catch {
155
+ payload = null;
156
+ }
157
+ const diagnostic = isVitals ? vitalsDiagnostic(payload) : browserDiagnostic(payload);
158
+ if (diagnostic == null) {
159
+ // The body was not the shape this path promises. Refused rather than
160
+ // guessed at: a diagnostic assembled out of a malformed report is a line
161
+ // in somebody's terminal that describes nothing.
162
+ response.statusCode = 400;
163
+ response.end();
164
+ return;
165
+ }
166
+
167
+ // Everything above either answered or handed the request on, so nothing
168
+ // below can leave one hanging — and an exception from `report` is the dev
169
+ // server's own failure rather than the page's, so it goes to Vite's error
170
+ // handler like any other. An `async` connect middleware whose rejection
171
+ // nobody catches is an unhandled rejection, which on a modern Node ends
172
+ // the process: `uf dev` would exit on a malformed telemetry post.
173
+ try {
174
+ report(diagnostic);
175
+ } catch (error) {
176
+ next(error);
177
+ return;
178
+ }
179
+ // No body, and nothing about the machine in the answer. The page posted
180
+ // this and is not owed a reading of it back.
181
+ response.statusCode = 204;
182
+ response.end();
183
+ };
184
+ }
185
+
186
+ /**
187
+ * One diagnostic a browser-side runtime produced, or `null`.
188
+ *
189
+ * Every field is checked and every string is bounded, because all of it is
190
+ * page-authored: a hydration mismatch on a page whose difference is in
191
+ * somebody's comment carries that comment into this terminal. Nothing here is
192
+ * interpreted — the terminal renderer prints text — but a report with a
193
+ * thousand lines in it would still scroll the reason for it off the screen.
194
+ *
195
+ * @param {unknown} payload
196
+ */
197
+ export function browserDiagnostic(payload) {
198
+ if (payload == null || typeof payload !== "object" || Array.isArray(payload)) return null;
199
+ const message = line(payload.message);
200
+ if (message === "") return null;
201
+
202
+ const severity = SEVERITIES.has(payload.severity) ? payload.severity : "error";
203
+ const diagnostic = { severity, message };
204
+ const origin = line(payload.url);
205
+ if (origin !== "") diagnostic.origin = origin;
206
+ const detail = lines(payload.detail);
207
+ if (detail.length > 0) diagnostic.detail = detail;
208
+ // A position, when the browser had one. It is what turns the status line
209
+ // into a code frame on the other side, and a browser that only knows "this
210
+ // component" rather than "this line" is expected: the frame is the better
211
+ // rendering when it is available and never a requirement.
212
+ const file = line(payload.file);
213
+ if (file !== "" && Number.isInteger(payload.line) && payload.line > 0) {
214
+ diagnostic.file = file;
215
+ diagnostic.line = payload.line;
216
+ if (Number.isInteger(payload.column) && payload.column >= 0) {
217
+ diagnostic.column = payload.column;
218
+ }
219
+ }
220
+ return diagnostic;
221
+ }
222
+
223
+ /**
224
+ * A `VitalsReport` as one diagnostic, or `null` when it carries no metric.
225
+ *
226
+ * One diagnostic per report rather than one per metric, because the beacon
227
+ * already coalesces across a microtask and a page load would otherwise be five
228
+ * separate lines interleaved with whatever else the terminal is saying. The
229
+ * severity is the worst rating in the report, which is the rule the issue asks
230
+ * for: a rating that is not `good` is the interesting one and has to read as
231
+ * one.
232
+ *
233
+ * @param {unknown} payload
234
+ */
235
+ export function vitalsDiagnostic(payload) {
236
+ if (payload == null || typeof payload !== "object" || Array.isArray(payload)) return null;
237
+ if (!Array.isArray(payload.vitals)) return null;
238
+
239
+ const measured = [];
240
+ for (const vital of payload.vitals.slice(0, MAX_VITALS)) {
241
+ if (vital == null || typeof vital !== "object") continue;
242
+ if (typeof vital.name !== "string" || typeof vital.value !== "number") continue;
243
+ if (!Number.isFinite(vital.value)) continue;
244
+ // The name and the rating are the page's strings, not this module's, even
245
+ // though a beacon written by `@uniflowed/web/vitals` only ever sends the
246
+ // five names and the three ratings. Anything can post here, so they go
247
+ // through the same bounding and control-character scrub as a diagnostic's
248
+ // own text, and a *word* has no business being longer than a word.
249
+ const name = line(vital.name).slice(0, MAX_NAME_CHARS);
250
+ if (name === "") continue;
251
+ const rating = line(vital.rating).slice(0, MAX_NAME_CHARS) || "unknown";
252
+ measured.push({ name, value: vital.value, rating });
253
+ }
254
+ if (measured.length === 0) return null;
255
+
256
+ // Worst first, so the line the reader needs is the line under the headline
257
+ // rather than wherever the browser happened to finish measuring.
258
+ measured.sort((left, right) => severityOf(right.rating) - severityOf(left.rating));
259
+ const worst = measured[0];
260
+ const severity = ratingSeverity(worst.rating);
261
+ const message =
262
+ severity === "info"
263
+ ? `web vitals: ${measured.map((vital) => vital.name).join(", ")} good`
264
+ : `web vitals: ${worst.name} is ${worst.rating} (${formatValue(worst)})`;
265
+
266
+ const diagnostic = {
267
+ severity,
268
+ message,
269
+ detail: measured.map((vital) => `${vital.name} ${formatValue(vital)} — ${vital.rating}`),
270
+ };
271
+ const origin = line(payload.url);
272
+ if (origin !== "") diagnostic.origin = origin;
273
+ return diagnostic;
274
+ }
275
+
276
+ /** How bad a rating is, for ordering; an unknown word sorts as the worst. */
277
+ function severityOf(rating) {
278
+ if (rating === "good") return 0;
279
+ if (rating === "needs-improvement") return 1;
280
+ return 2;
281
+ }
282
+
283
+ /** The channel severity a rating maps to. */
284
+ function ratingSeverity(rating) {
285
+ if (rating === "good") return "info";
286
+ if (rating === "needs-improvement") return "warn";
287
+ return "error";
288
+ }
289
+
290
+ /**
291
+ * A metric's value with its unit.
292
+ *
293
+ * CLS is a unitless layout-shift score and everything else is milliseconds,
294
+ * which is the one thing a reader has to know to act on the number — a `0.24`
295
+ * printed as `0.24 ms` reads as the best result in the report rather than a
296
+ * failing one.
297
+ */
298
+ function formatValue(vital) {
299
+ if (vital.name === "CLS") return String(Math.round(vital.value * 1000) / 1000);
300
+ return `${Math.round(vital.value)} ms`;
301
+ }
302
+
303
+ /** One bounded single-line string, or `""` for anything that is not one. */
304
+ function line(value) {
305
+ if (typeof value !== "string") return "";
306
+ return printable(value).trim();
307
+ }
308
+
309
+ /** A bounded list of bounded lines, from a string or an array of them. */
310
+ function lines(value) {
311
+ const source = typeof value === "string" ? value.split("\n") : value;
312
+ if (!Array.isArray(source)) return [];
313
+ const kept = [];
314
+ for (const entry of source) {
315
+ if (kept.length === MAX_DETAIL_LINES) {
316
+ kept.push("…");
317
+ break;
318
+ }
319
+ if (typeof entry !== "string") continue;
320
+ kept.push(printable(entry));
321
+ }
322
+ return kept;
323
+ }
324
+
325
+ /**
326
+ * One line of page-authored text, safe to write to a terminal and bounded.
327
+ *
328
+ * Every control character becomes a space, `ESC` included, and that is the
329
+ * point rather than tidiness: what is being rendered was written by a page, and
330
+ * a page that could put `ESC [` into a diagnostic could move the cursor,
331
+ * recolour the rest of the session or overwrite the line above its own report.
332
+ * `uf` owns this terminal — see `internal/events.js` — and nothing that arrives
333
+ * over a socket gets to draw on it.
334
+ *
335
+ * A scan rather than a regular expression, per `docs/security.md`'s "no regex
336
+ * on untrusted input": the rule is about backtracking and a character class
337
+ * cannot backtrack, but a loop needs no argument at all and is no longer.
338
+ */
339
+ function printable(value) {
340
+ let text = "";
341
+ for (const character of value.slice(0, MAX_LINE_CHARS)) {
342
+ const code = character.codePointAt(0);
343
+ text += code < 0x20 || code === 0x7f ? " " : character;
344
+ }
345
+ return text;
346
+ }
347
+
348
+ /**
349
+ * The whole request body, or `null` when it is over `limit`.
350
+ *
351
+ * Counted as it arrives rather than trusting `content-length`: the header is
352
+ * the sender's claim and the bytes are the fact, and `sendBeacon` sends
353
+ * neither a length this side should rely on nor a content type worth reading —
354
+ * a string payload goes out as `text/plain`, so the type says nothing about
355
+ * whether the body is the JSON both endpoints document.
356
+ */
357
+ async function readBody(request, limit) {
358
+ let size = 0;
359
+ const chunks = [];
360
+ for await (const chunk of request) {
361
+ size += chunk.length;
362
+ if (size > limit) return null;
363
+ chunks.push(chunk);
364
+ }
365
+ return Buffer.concat(chunks).toString("utf8");
366
+ }
@@ -54,11 +54,11 @@ export function stripAnsi(text) {
54
54
  /**
55
55
  * Report a page that rendered its error boundary instead of itself.
56
56
  *
57
- * `uf dev` has two renderers — the plugin's middleware and the driver's — and
58
- * this is the one place either of them says so, because a message written
59
- * twice is a message that ends up saying two things. The document the browser
60
- * gets is the application's error page, which is what a visitor would see;
61
- * the exception belongs in the terminal, which is uf's.
57
+ * The document the browser gets is the application's error page, which is what
58
+ * a visitor would see; the exception belongs in the terminal, which is uf's.
59
+ * There is one renderer under `uf dev` and therefore one caller of this — see
60
+ * `../index.js` — where there used to be two middlewares and a message that
61
+ * could be written twice.
62
62
  *
63
63
  * The stack is mapped back onto the Flow source first, so the frames name the
64
64
  * file that was written rather than the one that was compiled.
@@ -547,6 +547,25 @@ export const VIRTUAL = Object.freeze({
547
547
  * `virtual:uf/server` is generated with the default and always will be: the
548
548
  * server renders every route, so its table is the complete one.
549
549
  *
550
+ * # `relativeTo`, and the one string in this table a browser can read
551
+ *
552
+ * Every `import()` here is a specifier Vite resolves and rewrites to a chunk
553
+ * URL, so no absolute path survives the build — except `file`, which is a
554
+ * string. It is the route's source path, kept for diagnostics: the middleware
555
+ * table's is what names a module in an error, and `router.js`'s generated
556
+ * types are about the same files.
557
+ *
558
+ * The server's table can hold an absolute path; it is read on the machine that
559
+ * has those files. The browser's cannot, because that table is downloaded:
560
+ * uf's own manual shipped `/home/<user>/…/docs/app/guide/cache/_uf.page.mdx`
561
+ * for each of thirty-four routes to every visitor, which publishes the build
562
+ * machine's layout and its user's name for nothing — the browser has no
563
+ * filesystem to resolve them against and reads them only in a message.
564
+ *
565
+ * So the client call passes the project root and every `file` here is emitted
566
+ * relative to it. Diagnostics keep a path a person can act on — a shorter one
567
+ * — and a deploy stops describing the machine it was built on.
568
+ *
550
569
  * @param {{
551
570
  * routes: Route[],
552
571
  * handlers?: Handler[],
@@ -554,10 +573,29 @@ export const VIRTUAL = Object.freeze({
554
573
  * notFound?: NotFoundBoundary[],
555
574
  * errors?: ErrorBoundary[],
556
575
  * }} table
557
- * @param {{shipsPage?: (route: Route) => boolean}} [options]
576
+ * @param {{
577
+ * shipsPage?: (route: Route) => boolean,
578
+ * relativeTo?: string,
579
+ * }} [options]
558
580
  */
559
581
  export function routesModuleSource(table, options = {}) {
560
582
  const shipsPage = options.shipsPage ?? (() => true);
583
+ const relativeTo = options.relativeTo ?? null;
584
+ /**
585
+ * A `file` as this table should state it.
586
+ *
587
+ * Relative even when that means leading `..` segments — a module outside the
588
+ * project root is rare and a `../` path still says where it is without
589
+ * saying where the machine is, which is the whole property. Separators are
590
+ * POSIX because this string is read wherever the bundle is opened rather
591
+ * than where it was written.
592
+ */
593
+ const displayFile = (file) => {
594
+ if (relativeTo == null || file == null) {
595
+ return file;
596
+ }
597
+ return path.relative(relativeTo, file).split(path.sep).join("/");
598
+ };
561
599
  const layoutIds = new Map();
562
600
  const layoutImports = [];
563
601
  const layoutId = (file) => {
@@ -616,7 +654,7 @@ export function routesModuleSource(table, options = {}) {
616
654
  path: ${JSON.stringify(route.path)},
617
655
  params: ${JSON.stringify(route.params)},
618
656
  mdx: ${route.mdx},
619
- file: ${JSON.stringify(route.page)},
657
+ file: ${JSON.stringify(displayFile(route.page))},
620
658
  layouts: [],
621
659
  loading: [],
622
660
  templates: [],
@@ -633,7 +671,7 @@ export function routesModuleSource(table, options = {}) {
633
671
  path: ${JSON.stringify(route.path)},
634
672
  params: ${JSON.stringify(route.params)},
635
673
  mdx: ${route.mdx},
636
- file: ${JSON.stringify(route.page)},
674
+ file: ${JSON.stringify(displayFile(route.page))},
637
675
  page: () => import(${JSON.stringify(route.page)}),
638
676
  layouts: [${layouts.join(", ")}],
639
677
  loading: [${loading.join(", ")}],
@@ -648,7 +686,7 @@ export function routesModuleSource(table, options = {}) {
648
686
  const SYNTHESISED = JSON.stringify("@uniflowed/router");
649
687
  const boundaryModule = (file) =>
650
688
  file == null ? "null" : `() => import(${JSON.stringify(file)})`;
651
- const boundaryFile = (file) => (file == null ? SYNTHESISED : JSON.stringify(file));
689
+ const boundaryFile = (file) => (file == null ? SYNTHESISED : JSON.stringify(displayFile(file)));
652
690
 
653
691
  // A list, because a not-found is a segment file: every directory may declare
654
692
  // one and the router takes the nearest above the path. `layoutId` is the
@@ -684,7 +722,7 @@ export function routesModuleSource(table, options = {}) {
684
722
  (handler) => ` {
685
723
  path: ${JSON.stringify(handler.path)},
686
724
  params: ${JSON.stringify(handler.params)},
687
- file: ${JSON.stringify(handler.module)},
725
+ file: ${JSON.stringify(displayFile(handler.module))},
688
726
  load: () => import(${JSON.stringify(handler.module)}),
689
727
  }`,
690
728
  );
@@ -697,7 +735,7 @@ export function routesModuleSource(table, options = {}) {
697
735
  const middlewareEntries = (table.middleware ?? []).map(
698
736
  (entry) => ` {
699
737
  path: ${JSON.stringify(entry.path)},
700
- file: ${JSON.stringify(entry.module)},
738
+ file: ${JSON.stringify(displayFile(entry.module))},
701
739
  load: () => import(${JSON.stringify(entry.module)}),
702
740
  }`,
703
741
  );
package/internal/serve.js CHANGED
@@ -245,21 +245,7 @@ export function assetsFromManifest(manifest) {
245
245
  * @param {() => Promise<mixed>} body
246
246
  */
247
247
  export async function withRequest(entry, request, body) {
248
- const lifecycle = entry.beginRequest(request);
249
- const { run, settle } = lifecycle;
250
- // What this host can do, put on the request the way `createFetchHandler`
251
- // puts it on the one it owns. `uf dev` and `uf build --compile` reach a
252
- // route handler without going through that function, and a handler that
253
- // streams events or queues work has to get the same answer from all four
254
- // front doors — a capability that is present under `uf start` and absent
255
- // under `uf dev` is the difference this whole seam exists to remove.
256
- //
257
- // `nodeCapabilities`, because both of those *are* a Node process with a
258
- // socket: a body reaches the client as it is written, and the process is
259
- // still there afterwards. Neither passes an upgrader or a queue, because uf
260
- // defines both and implements neither.
261
- const { nodeCapabilities } = await deployment();
262
- lifecycle.context.capabilities ??= nodeCapabilities();
248
+ const { run, settle } = await beginRequest(entry, request);
263
249
  try {
264
250
  return await run(body);
265
251
  } finally {
@@ -267,6 +253,44 @@ export async function withRequest(entry, request, body) {
267
253
  }
268
254
  }
269
255
 
256
+ /**
257
+ * Begin a request on this host, with what this host can do already on it.
258
+ *
259
+ * The half of [`withRequest`] that a caller which may *not* answer needs.
260
+ * `uf dev` runs the application's middleware, its action endpoint and its
261
+ * dispatcher for every request, and hands the ones none of them claimed back
262
+ * to Vite's chain — at which point the response is written somewhere this
263
+ * module cannot see, so settling has to wait for the socket rather than for a
264
+ * `finally` here. A caller that always answers should use [`withRequest`] and
265
+ * not think about it.
266
+ *
267
+ * `entry.beginRequest` and not an import: the request lives in an
268
+ * `AsyncLocalStorage` belonging to one copy of `@uniflowed/server`, and the
269
+ * copy that matters is the one inside the application bundle. See
270
+ * `serverModuleSource` in `./routes.js`.
271
+ *
272
+ * What this host can do is put on the request the way `createFetchHandler`
273
+ * puts it on the one it owns. `uf dev` and `uf build --compile` reach a route
274
+ * handler without going through that function, and a handler that streams
275
+ * events or queues work has to get the same answer from all four front doors —
276
+ * a capability that is present under `uf start` and absent under `uf dev` is
277
+ * the difference this whole seam exists to remove.
278
+ *
279
+ * `nodeCapabilities`, because both of those *are* a Node process with a
280
+ * socket: a body reaches the client as it is written, and the process is still
281
+ * there afterwards. Neither passes an upgrader or a queue, because uf defines
282
+ * both and implements neither.
283
+ *
284
+ * @param {{beginRequest: (request: Request) => {context: object, run: <T>(body: () => Promise<T>) => Promise<T>, settle: () => Promise<void>}}} entry
285
+ * @param {Request} request
286
+ */
287
+ export async function beginRequest(entry, request) {
288
+ const lifecycle = entry.beginRequest(request);
289
+ const { nodeCapabilities } = await deployment();
290
+ lifecycle.context.capabilities ??= nodeCapabilities();
291
+ return lifecycle;
292
+ }
293
+
270
294
  /**
271
295
  * The application half: route handlers, then rendering.
272
296
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniflowed/vite",
3
- "version": "0.0.0-alpha.13",
3
+ "version": "0.0.0-alpha.14",
4
4
  "description": "Vite, driven by uf.config.js: every Flow module through `uf transform`, MDX, the file-system router and static rendering as Vite plugins.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -10,6 +10,14 @@
10
10
  "url": "git+https://github.com/ubugeeei-prod/uf.git",
11
11
  "directory": "packages/vite"
12
12
  },
13
+ "uf": {
14
+ "builder": {
15
+ "driver": "./driver.js",
16
+ "preload": {
17
+ "bun": "@uniflowed/host/bun-preload"
18
+ }
19
+ }
20
+ },
13
21
  "exports": {
14
22
  ".": "./index.js",
15
23
  "./driver": "./driver.js",
@@ -25,8 +33,8 @@
25
33
  "dependencies": {
26
34
  "@mdx-js/rollup": "^3.1.1",
27
35
  "@shikijs/rehype": "^3.23.0",
28
- "@uniflowed/host": "0.0.0-alpha.13",
29
- "@uniflowed/server": "0.0.0-alpha.13",
36
+ "@uniflowed/host": "0.0.0-alpha.14",
37
+ "@uniflowed/server": "0.0.0-alpha.14",
30
38
  "rehype-slug": "^6.0.0",
31
39
  "remark-frontmatter": "^5.0.0",
32
40
  "remark-gfm": "^4.0.1",